From 24cb0ae90588b8cf38240ce5e2da18073f200512 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 11 Nov 2016 17:28:03 +0100 Subject: [PATCH 01/15] Wallet names shouldn't include address. (Actually wallet files shouldn't contain it either, but we'll leave that for a later PR). --- ethstore/src/dir/disk.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ethstore/src/dir/disk.rs b/ethstore/src/dir/disk.rs index 6616ec15d..56b2c1ccb 100644 --- a/ethstore/src/dir/disk.rs +++ b/ethstore/src/dir/disk.rs @@ -20,6 +20,7 @@ use std::collections::HashMap; use time; use ethkey::Address; use {json, SafeAccount, Error}; +use json::UUID; use super::KeyDirectory; const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json"]; @@ -112,7 +113,7 @@ impl KeyDirectory for DiskDirectory { // build file path let filename = account.filename.as_ref().cloned().unwrap_or_else(|| { let timestamp = time::strftime("%Y-%m-%dT%H-%M-%S", &time::now_utc()).expect("Time-format string is valid."); - format!("UTC--{}Z--{:?}", timestamp, account.address) + format!("UTC--{}Z--{}", timestamp, UUID::from(account.id)) }); // update account filename From aa147461b07cf704df873ec45880e68943e5cda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 11 Nov 2016 17:38:45 +0100 Subject: [PATCH 02/15] Generating browser link on signer new-token --- parity/configuration.rs | 4 +++- parity/signer.rs | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/parity/configuration.rs b/parity/configuration.rs index 75d319272..928e9ba7a 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -104,7 +104,9 @@ impl Configuration { Cmd::Version } else if self.args.cmd_signer && self.args.cmd_new_token { Cmd::SignerToken(SignerCommand { - path: dirs.signer + path: dirs.signer, + signer_interface: signer_conf.interface, + signer_port: signer_conf.port, }) } else if self.args.cmd_tools && self.args.cmd_hash { Cmd::Hash(self.args.arg_file) diff --git a/parity/signer.rs b/parity/signer.rs index a26cc431a..fd1258bf6 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -71,12 +71,23 @@ fn codes_path(path: String) -> PathBuf { #[derive(Debug, PartialEq)] pub struct SignerCommand { pub path: String, + pub signer_interface: String, + pub signer_port: u16, } pub fn execute(cmd: SignerCommand) -> Result { - generate_new_token(cmd.path) - .map(|code| format!("This key code will authorise your System Signer UI: {}", Colour::White.bold().paint(code))) - .map_err(|err| format!("Error generating token: {:?}", err)) + let code = try!(generate_new_token(cmd.path).map_err(|err| format!("Error generating token: {:?}", err))); + + let url = format!("http://{}:{}/#/auth?token={}", cmd.signer_interface, cmd.signer_port, code); + Ok(format!( + r#" +Open: {} +to authorize your browser. +Or use the code: {} + "#, + Colour::White.bold().paint(url), + code + )) } pub fn generate_new_token(path: String) -> io::Result { From be6023b6020aec54563b9e7beea49c58787702af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 11 Nov 2016 17:56:52 +0100 Subject: [PATCH 03/15] Opening a browser --- parity/signer.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/parity/signer.rs b/parity/signer.rs index fd1258bf6..835cc397a 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -20,6 +20,7 @@ use std::path::PathBuf; use ansi_term::Colour; use io::{ForwardPanic, PanicHandler}; use util::path::restrict_permissions_owner; +use url; use rpc_apis; use ethcore_signer as signer; use helpers::replace_home; @@ -77,15 +78,16 @@ pub struct SignerCommand { pub fn execute(cmd: SignerCommand) -> Result { let code = try!(generate_new_token(cmd.path).map_err(|err| format!("Error generating token: {:?}", err))); - - let url = format!("http://{}:{}/#/auth?token={}", cmd.signer_interface, cmd.signer_port, code); + let auth_url = format!("http://{}:{}/#/auth?token={}", cmd.signer_interface, cmd.signer_port, code); + // Open a browser + url::open(&auth_url); + // And print in to the console Ok(format!( r#" Open: {} to authorize your browser. -Or use the code: {} - "#, - Colour::White.bold().paint(url), +Or use the code: {}"#, + Colour::White.bold().paint(auth_url), code )) } From ba699fdb2569498115dd1a349c6801d160aaedca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 11 Nov 2016 18:02:36 +0100 Subject: [PATCH 04/15] new line [ci:skip] --- parity/signer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/parity/signer.rs b/parity/signer.rs index 835cc397a..09bab3de9 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -86,7 +86,8 @@ pub fn execute(cmd: SignerCommand) -> Result { r#" Open: {} to authorize your browser. -Or use the code: {}"#, +Or use the code: +{}"#, Colour::White.bold().paint(auth_url), code )) From 7ca317912fc7ea7eec420560de6adfc8cdb0f222 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Sat, 12 Nov 2016 17:09:55 +0100 Subject: [PATCH 05/15] Set signer token via #/auth=token={} --- js/src/index.js | 10 +++++++++- js/src/secureApi.js | 25 +++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/js/src/index.js b/js/src/index.js index c0f4f94ad..fda785842 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -25,6 +25,7 @@ import ReactDOM from 'react-dom'; import injectTapEventPlugin from 'react-tap-event-plugin'; import { createHashHistory } from 'history'; import { Redirect, Router, Route, useRouterHistory } from 'react-router'; +import qs from 'querystring'; import SecureApi from './secureApi'; import ContractInstances from './contracts'; @@ -45,6 +46,7 @@ import './index.html'; injectTapEventPlugin(); +const AUTH_HASH = '#/auth?'; const parityUrl = process.env.PARITY_URL || ( process.env.NODE_ENV === 'production' @@ -52,7 +54,12 @@ const parityUrl = process.env.PARITY_URL || : '127.0.0.1:8180' ); -const api = new SecureApi(`ws://${parityUrl}`); +let token = null; +if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) { + token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token; +} + +const api = new SecureApi(`ws://${parityUrl}`, token); ContractInstances.create(api); const store = initStore(api); @@ -67,6 +74,7 @@ ReactDOM.render( + diff --git a/js/src/secureApi.js b/js/src/secureApi.js index ed665a5e6..67b38ff1b 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -19,23 +19,36 @@ import Api from './api'; const sysuiToken = window.localStorage.getItem('sysuiToken'); export default class SecureApi extends Api { - constructor (url) { - super(new Api.Transport.Ws(url, sysuiToken)); + constructor (url, _token) { + super(new Api.Transport.Ws(url, SecureApi.sanitizeToken(_token || sysuiToken))); + + const token = _token || sysuiToken; this._isConnecting = true; - this._connectState = sysuiToken === 'initial' ? 1 : 0; + this._connectState = token === 'initial' ? 1 : 0; this._needsToken = false; this._dappsPort = 8080; this._dappsInterface = null; this._signerPort = 8180; - console.log('SecureApi:constructor', sysuiToken); + console.log('SecureApi:constructor', token); + this.storeToken(token); this._followConnection(); } + static sanitizeToken (token) { + return token + ? token.replace(/[^a-zA-Z0-9]/g, '') + : null; + } + + storeToken (token) { + window.localStorage.setItem('sysuiToken', SecureApi.sanitizeToken(token)); + } + setToken = () => { - window.localStorage.setItem('sysuiToken', this._transport.token); + this.storeToken(this._transport.token); console.log('SecureApi:setToken', this._transport.token); } @@ -115,7 +128,7 @@ export default class SecureApi extends Api { updateToken (token, connectState = 0) { this._connectState = connectState; - this._transport.updateToken(token.replace(/[^a-zA-Z0-9]/g, '')); + this._transport.updateToken(SecureApi.sanitizeToken(token)); this._followConnection(); console.log('SecureApi:updateToken', this._transport.token, connectState); } From 665504c4140f974b0ab3c0283a6482a6de2e730a Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 14 Nov 2016 12:10:03 +0100 Subject: [PATCH 06/15] Only use #/auth token as fallback --- js/src/secureApi.js | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/js/src/secureApi.js b/js/src/secureApi.js index 67b38ff1b..3e1d9eab0 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -19,36 +19,24 @@ import Api from './api'; const sysuiToken = window.localStorage.getItem('sysuiToken'); export default class SecureApi extends Api { - constructor (url, _token) { - super(new Api.Transport.Ws(url, SecureApi.sanitizeToken(_token || sysuiToken))); - - const token = _token || sysuiToken; + constructor (url, nextToken) { + super(new Api.Transport.Ws(url, sysuiToken)); this._isConnecting = true; - this._connectState = token === 'initial' ? 1 : 0; + this._connectState = sysuiToken === 'initial' ? 1 : 0; this._needsToken = false; + this._nextToken = nextToken; this._dappsPort = 8080; this._dappsInterface = null; this._signerPort = 8180; - console.log('SecureApi:constructor', token); + console.log('SecureApi:constructor', sysuiToken); - this.storeToken(token); this._followConnection(); } - static sanitizeToken (token) { - return token - ? token.replace(/[^a-zA-Z0-9]/g, '') - : null; - } - - storeToken (token) { - window.localStorage.setItem('sysuiToken', SecureApi.sanitizeToken(token)); - } - setToken = () => { - this.storeToken(this._transport.token); + window.localStorage.setItem('sysuiToken', this._transport.token); console.log('SecureApi:setToken', this._transport.token); } @@ -70,7 +58,11 @@ export default class SecureApi extends Api { if (isConnected) { return this.connectSuccess(); } else if (lastError) { - this.updateToken('initial', 1); + const nextToken = this._nextToken || 'initial'; + const nextState = this._nextToken ? 0 : 1; + + this._nextToken = null; + this.updateToken(nextToken, nextState); } break; @@ -128,7 +120,7 @@ export default class SecureApi extends Api { updateToken (token, connectState = 0) { this._connectState = connectState; - this._transport.updateToken(SecureApi.sanitizeToken(token)); + this._transport.updateToken(token.replace(/[^a-zA-Z0-9]/g, '')); this._followConnection(); console.log('SecureApi:updateToken', this._transport.token, connectState); } From 7f011afacbbb30fb9c3840b302e61062a62d51e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 14 Nov 2016 11:56:01 +0100 Subject: [PATCH 07/15] Tokens retention policy --- parity/configuration.rs | 20 ++-- parity/run.rs | 25 +++-- parity/signer.rs | 16 ++-- signer/src/authcode_store.rs | 160 ++++++++++++++++++++++++++++---- signer/src/ws_server/session.rs | 3 + 5 files changed, 181 insertions(+), 43 deletions(-) diff --git a/parity/configuration.rs b/parity/configuration.rs index 928e9ba7a..61063aa18 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -35,7 +35,7 @@ use params::{ResealPolicy, AccountsConfig, GasPricerConfig, MinerExtras}; use ethcore_logger::Config as LogConfig; use dir::Directories; use dapps::Configuration as DappsConfiguration; -use signer::{Configuration as SignerConfiguration, SignerCommand}; +use signer::{Configuration as SignerConfiguration}; use run::RunCmd; use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat}; use presale::ImportWallet; @@ -49,7 +49,7 @@ pub enum Cmd { Account(AccountCmd), ImportPresaleWallet(ImportWallet), Blockchain(BlockchainCmd), - SignerToken(SignerCommand), + SignerToken(SignerConfiguration), Snapshot(SnapshotCommand), Hash(Option), } @@ -103,11 +103,7 @@ impl Configuration { let cmd = if self.args.flag_version { Cmd::Version } else if self.args.cmd_signer && self.args.cmd_new_token { - Cmd::SignerToken(SignerCommand { - path: dirs.signer, - signer_interface: signer_conf.interface, - signer_port: signer_conf.port, - }) + Cmd::SignerToken(signer_conf) } else if self.args.cmd_tools && self.args.cmd_hash { Cmd::Hash(self.args.arg_file) } else if self.args.cmd_account { @@ -692,7 +688,7 @@ mod tests { use ethcore::miner::{MinerOptions, PrioritizationStrategy}; use helpers::{replace_home, default_network_config}; use run::RunCmd; - use signer::{Configuration as SignerConfiguration, SignerCommand}; + use signer::{Configuration as SignerConfiguration}; use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat}; use presale::ImportWallet; use account::{AccountCmd, NewAccount, ImportAccounts}; @@ -829,8 +825,12 @@ mod tests { let args = vec!["parity", "signer", "new-token"]; let conf = parse(&args); let expected = replace_home("$HOME/.parity/signer"); - assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(SignerCommand { - path: expected, + assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(SignerConfiguration { + enabled: true, + signer_path: expected, + interface: "127.0.0.1".into(), + port: 8180, + skip_origin_validation: false, })); } diff --git a/parity/run.rs b/parity/run.rs index 56ff92c25..085fcfa34 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -48,7 +48,6 @@ use signer; use modules; use rpc_apis; use rpc; -use url; // how often to take periodic snapshots. const SNAPSHOT_PERIOD: u64 = 10000; @@ -93,13 +92,26 @@ pub struct RunCmd { pub check_seal: bool, } +pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> { + if !dapps_conf.enabled { + return Err("Cannot use UI command with Dapps turned off.".into()) + } + + if !signer_conf.enabled { + return Err("Cannot use UI command with UI turned off.".into()) + } + + let token = try!(signer::generate_token_and_open_ui(signer_conf)); + println!("{}", token); + Ok(()) +} + pub fn execute(cmd: RunCmd, logger: Arc) -> Result<(), String> { if cmd.ui && cmd.dapps_conf.enabled { // Check if Parity is already running let addr = format!("{}:{}", cmd.dapps_conf.interface, cmd.dapps_conf.port); if !TcpListener::bind(&addr as &str).is_ok() { - url::open(&format!("http://{}:{}/", cmd.dapps_conf.interface, cmd.dapps_conf.port)); - return Ok(()); + return open_ui(&cmd.dapps_conf, &cmd.signer_conf); } } @@ -309,7 +321,7 @@ pub fn execute(cmd: RunCmd, logger: Arc) -> Result<(), String> { }; // start signer server - let signer_server = try!(signer::start(cmd.signer_conf, signer_deps)); + let signer_server = try!(signer::start(cmd.signer_conf.clone(), signer_deps)); let informant = Arc::new(Informant::new( service.client(), @@ -363,10 +375,7 @@ pub fn execute(cmd: RunCmd, logger: Arc) -> Result<(), String> { // start ui if cmd.ui { - if !cmd.dapps_conf.enabled { - return Err("Cannot use UI command with Dapps turned off.".into()) - } - url::open(&format!("http://{}:{}/", cmd.dapps_conf.interface, cmd.dapps_conf.port)); + try!(open_ui(&cmd.dapps_conf, &cmd.signer_conf)); } // Handle exit diff --git a/parity/signer.rs b/parity/signer.rs index 09bab3de9..4ce94eae4 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -28,7 +28,7 @@ pub use ethcore_signer::Server as SignerServer; const CODES_FILENAME: &'static str = "authcodes"; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct Configuration { pub enabled: bool, pub port: u16, @@ -69,16 +69,13 @@ fn codes_path(path: String) -> PathBuf { p } -#[derive(Debug, PartialEq)] -pub struct SignerCommand { - pub path: String, - pub signer_interface: String, - pub signer_port: u16, +pub fn execute(cmd: Configuration) -> Result { + generate_token_and_open_ui(&cmd) } -pub fn execute(cmd: SignerCommand) -> Result { - let code = try!(generate_new_token(cmd.path).map_err(|err| format!("Error generating token: {:?}", err))); - let auth_url = format!("http://{}:{}/#/auth?token={}", cmd.signer_interface, cmd.signer_port, code); +pub fn generate_token_and_open_ui(conf: &Configuration) -> Result { + let code = try!(generate_new_token(conf.signer_path.clone()).map_err(|err| format!("Error generating token: {:?}", err))); + let auth_url = format!("http://{}:{}/#/auth?token={}", conf.interface, conf.port, code); // Open a browser url::open(&auth_url); // And print in to the console @@ -96,6 +93,7 @@ Or use the code: pub fn generate_new_token(path: String) -> io::Result { let path = codes_path(path); let mut codes = try!(signer::AuthCodes::from_file(&path)); + codes.clear_garbage(); let code = try!(codes.generate_new()); try!(codes.to_file(&path)); trace!("New key code created: {}", Colour::White.bold().paint(&code[..])); diff --git a/signer/src/authcode_store.rs b/signer/src/authcode_store.rs index d8474441c..cbb78db41 100644 --- a/signer/src/authcode_store.rs +++ b/signer/src/authcode_store.rs @@ -16,12 +16,10 @@ use rand::Rng; use rand::os::OsRng; -use std::io; -use std::io::{Read, Write}; -use std::fs; +use std::io::{self, Read, Write}; use std::path::Path; -use std::time; -use util::{H256, Hashable}; +use std::{fs, time, mem}; +use util::{H256, Hashable, Itertools}; /// Providing current time in seconds pub trait TimeProvider { @@ -47,12 +45,35 @@ impl TimeProvider for DefaultTimeProvider { /// No of seconds the hash is valid const TIME_THRESHOLD: u64 = 7; +/// minimal length of hash const TOKEN_LENGTH: usize = 16; +/// special "initial" token used for authorization when there are no tokens yet. const INITIAL_TOKEN: &'static str = "initial"; +/// Separator between fields in serialized tokens file. +const SEPARATOR: &'static str = ";"; +/// Number of seconds to keep unused tokens. +const UNUSED_TOKEN_TIMEOUT: u64 = 3600 * 24; // a day + +struct Code { + code: String, + /// Duration since unix_epoch + created_at: time::Duration, + /// Duration since unix_epoch + last_used_at: Option, +} + +fn decode_time(val: &str) -> Option { + let time = val.parse::().ok(); + time.map(time::Duration::from_secs) +} + +fn encode_time(time: time::Duration) -> String { + format!("{}", time.as_secs()) +} /// Manages authorization codes for `SignerUIs` pub struct AuthCodes { - codes: Vec, + codes: Vec, now: T, } @@ -69,13 +90,32 @@ impl AuthCodes { "".into() } }; + let time_provider = DefaultTimeProvider::default(); + let codes = content.lines() - .filter(|f| f.len() >= TOKEN_LENGTH) - .map(String::from) + .filter_map(|line| { + let mut parts = line.split(SEPARATOR); + let token = parts.next(); + let created = parts.next(); + let used = parts.next(); + + match token { + None => None, + Some(token) if token.len() < TOKEN_LENGTH => None, + Some(token) => { + Some(Code { + code: token.into(), + last_used_at: used.and_then(decode_time), + created_at: created.and_then(decode_time) + .unwrap_or_else(|| time::Duration::from_secs(time_provider.now())), + }) + } + } + }) .collect(); Ok(AuthCodes { codes: codes, - now: DefaultTimeProvider::default(), + now: time_provider, }) } @@ -86,19 +126,30 @@ impl AuthCodes { /// Writes all `AuthCodes` to a disk. pub fn to_file(&self, file: &Path) -> io::Result<()> { let mut file = try!(fs::File::create(file)); - let content = self.codes.join("\n"); + let content = self.codes.iter().map(|code| { + let mut data = vec![code.code.clone(), encode_time(code.created_at.clone())]; + if let Some(used_at) = code.last_used_at.clone() { + data.push(encode_time(used_at)); + } + data.join(SEPARATOR) + }).join("\n"); file.write_all(content.as_bytes()) } /// Creates a new `AuthCodes` store with given `TimeProvider`. pub fn new(codes: Vec, now: T) -> Self { AuthCodes { - codes: codes, + codes: codes.into_iter().map(|code| Code { + code: code, + created_at: time::Duration::from_secs(now.now()), + last_used_at: None, + }).collect(), now: now, } } - /// Checks if given hash is correct identifier of `SignerUI` + /// Checks if given hash is correct authcode of `SignerUI` + /// Updates this hash last used field in case it's valid. #[cfg_attr(feature="dev", allow(wrong_self_convention))] pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool { let now = self.now.now(); @@ -121,8 +172,14 @@ impl AuthCodes { } // look for code - self.codes.iter() - .any(|code| &as_token(code) == hash) + for mut code in &mut self.codes { + if &as_token(&code.code) == hash { + code.last_used_at = Some(time::Duration::from_secs(now)); + return true; + } + } + + false } /// Generates and returns a new code that can be used by `SignerUIs` @@ -135,7 +192,11 @@ impl AuthCodes { .collect::>() .join("-"); trace!(target: "signer", "New authentication token generated."); - self.codes.push(code); + self.codes.push(Code { + code: code, + created_at: time::Duration::from_secs(self.now.now()), + last_used_at: None, + }); Ok(readable_code) } @@ -143,12 +204,31 @@ impl AuthCodes { pub fn is_empty(&self) -> bool { self.codes.is_empty() } -} + /// Removes old tokens that have not been used since creation. + pub fn clear_garbage(&mut self) { + let now = self.now.now(); + let threshold = time::Duration::from_secs(now.saturating_sub(UNUSED_TOKEN_TIMEOUT)); + + let codes = mem::replace(&mut self.codes, Vec::new()); + for code in codes { + // Skip codes that are old and were never used. + if code.last_used_at.is_none() && code.created_at <= threshold { + continue; + } + self.codes.push(code); + } + } +} #[cfg(test)] mod tests { + use devtools; + use std::io::{Read, Write}; + use std::{time, fs}; + use std::cell::Cell; + use util::{H256, Hashable}; use super::*; @@ -217,6 +297,54 @@ mod tests { assert_eq!(res2, false); } + #[test] + fn should_read_old_format_from_file() { + // given + let path = devtools::RandomTempPath::new(); + let code = "23521352asdfasdfadf"; + { + let mut file = fs::File::create(&path).unwrap(); + file.write_all(b"a\n23521352asdfasdfadf\nb\n").unwrap(); + } + + // when + let mut authcodes = AuthCodes::from_file(&path).unwrap(); + let time = time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + + // then + assert!(authcodes.is_valid(&generate_hash(code, time), time), "Code should be read from file"); + } + + #[test] + fn should_remove_old_unused_tokens() { + // given + let path = devtools::RandomTempPath::new(); + let code1 = "11111111asdfasdf111"; + let code2 = "22222222asdfasdf222"; + let code3 = "33333333asdfasdf333"; + + let time = Cell::new(100); + let mut codes = AuthCodes::new(vec![code1.into(), code2.into(), code3.into()], || time.get()); + // `code2` should not be removed (we never remove tokens that were used) + codes.is_valid(&generate_hash(code2, time.get()), time.get()); + + // when + time.set(100 + 10_000_000); + // mark `code1` as used now + codes.is_valid(&generate_hash(code1, time.get()), time.get()); + + let new_code = codes.generate_new().unwrap().replace('-', ""); + codes.clear_garbage(); + codes.to_file(&path).unwrap(); + + // then + let mut content = String::new(); + let mut file = fs::File::open(&path).unwrap(); + file.read_to_string(&mut content).unwrap(); + + assert_eq!(content, format!("{};100;10000100\n{};100;100\n{};10000100", code1, code2, new_code)); + } + } diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index b99ef48ef..5adc3fa80 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -94,6 +94,9 @@ fn auth_is_valid(codes_path: &Path, protocols: ws::Result>) -> bool { // Check if the code is valid AuthCodes::from_file(codes_path) .map(|mut codes| { + // remove old tokens + codes.clear_garbage(); + let res = codes.is_valid(&auth, time); // make sure to save back authcodes - it might have been modified if let Err(_) = codes.to_file(codes_path) { From 29271383dd9a03ccf63fee1a64ff041d33b7cb76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 16 Nov 2016 09:37:48 +0100 Subject: [PATCH 08/15] signer new-token doesn't open the browser --- parity/run.rs | 8 ++++++-- parity/signer.rs | 29 ++++++++++++++++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/parity/run.rs b/parity/run.rs index 085fcfa34..22d00fb1b 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -48,6 +48,7 @@ use signer; use modules; use rpc_apis; use rpc; +use url; // how often to take periodic snapshots. const SNAPSHOT_PERIOD: u64 = 10000; @@ -101,8 +102,11 @@ pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configur return Err("Cannot use UI command with UI turned off.".into()) } - let token = try!(signer::generate_token_and_open_ui(signer_conf)); - println!("{}", token); + let token = try!(signer::generate_token_and_url(signer_conf)); + // Open a browser + url::open(&token.url); + // Print a message + println!("{}", token.message); Ok(()) } diff --git a/parity/signer.rs b/parity/signer.rs index 4ce94eae4..6905fbb3c 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -20,7 +20,6 @@ use std::path::PathBuf; use ansi_term::Colour; use io::{ForwardPanic, PanicHandler}; use util::path::restrict_permissions_owner; -use url; use rpc_apis; use ethcore_signer as signer; use helpers::replace_home; @@ -54,6 +53,12 @@ pub struct Dependencies { pub apis: Arc, } +pub struct NewToken { + pub token: String, + pub url: String, + pub message: String, +} + pub fn start(conf: Configuration, deps: Dependencies) -> Result, String> { if !conf.enabled { Ok(None) @@ -70,24 +75,26 @@ fn codes_path(path: String) -> PathBuf { } pub fn execute(cmd: Configuration) -> Result { - generate_token_and_open_ui(&cmd) + Ok(try!(generate_token_and_url(&cmd)).message) } -pub fn generate_token_and_open_ui(conf: &Configuration) -> Result { +pub fn generate_token_and_url(conf: &Configuration) -> Result { let code = try!(generate_new_token(conf.signer_path.clone()).map_err(|err| format!("Error generating token: {:?}", err))); let auth_url = format!("http://{}:{}/#/auth?token={}", conf.interface, conf.port, code); - // Open a browser - url::open(&auth_url); // And print in to the console - Ok(format!( - r#" + Ok(NewToken { + token: code.clone(), + url: auth_url.clone(), + message: format!( + r#" Open: {} to authorize your browser. -Or use the code: +Or use the generated token: {}"#, - Colour::White.bold().paint(auth_url), - code - )) + Colour::White.bold().paint(auth_url), + code + ) + }) } pub fn generate_new_token(path: String) -> io::Result { From 6f2c818f9ff565d0e3e549a7b1e51f2b03f564ed Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 16 Nov 2016 16:43:21 +0100 Subject: [PATCH 09/15] Make tokenreg dapp fast again (#3474) * Using proper TokenReg Instance in TokenReg dApp #3371 * remove unnecessary logs in tokereg dapp * Improved Redux managment in TokeReg dApp #3371 * Fixfing linting --- js/src/contracts/contracts.js | 6 ++ js/src/contracts/githubhint.js | 32 ++++++++ js/src/contracts/registry.js | 10 ++- js/src/contracts/tokenreg.js | 6 +- js/src/dapps/tokenreg/Accounts/actions.js | 2 - js/src/dapps/tokenreg/Actions/Query/query.js | 12 +-- js/src/dapps/tokenreg/Actions/actions.js | 37 ---------- js/src/dapps/tokenreg/Actions/component.js | 2 - js/src/dapps/tokenreg/Actions/container.js | 5 +- .../tokenreg/Application/application.css | 1 + js/src/dapps/tokenreg/Status/actions.js | 58 +++++---------- js/src/dapps/tokenreg/Status/reducer.js | 4 +- js/src/dapps/tokenreg/Tokens/Token/index.js | 2 +- js/src/dapps/tokenreg/Tokens/Token/token.js | 27 ++++++- .../tokenreg/Tokens/Token/tokenContainer.js | 73 +++++++++++++++++++ js/src/dapps/tokenreg/Tokens/actions.js | 22 +----- js/src/dapps/tokenreg/Tokens/container.js | 29 ++------ js/src/dapps/tokenreg/Tokens/tokens.js | 27 ++----- 18 files changed, 187 insertions(+), 168 deletions(-) create mode 100644 js/src/contracts/githubhint.js create mode 100644 js/src/dapps/tokenreg/Tokens/Token/tokenContainer.js diff --git a/js/src/contracts/contracts.js b/js/src/contracts/contracts.js index c9d1c2390..a04321c7b 100644 --- a/js/src/contracts/contracts.js +++ b/js/src/contracts/contracts.js @@ -18,6 +18,7 @@ import DappReg from './dappreg'; import Registry from './registry'; import SignatureReg from './signaturereg'; import TokenReg from './tokenreg'; +import GithubHint from './githubhint'; let instance = null; @@ -30,6 +31,7 @@ export default class Contracts { this._dappreg = new DappReg(api, this._registry); this._signaturereg = new SignatureReg(api, this._registry); this._tokenreg = new TokenReg(api, this._registry); + this._githubhint = new GithubHint(api, this._registry); } get registry () { @@ -48,6 +50,10 @@ export default class Contracts { return this._tokenreg; } + get githubHint () { + return this._githubhint; + } + static create (api) { return new Contracts(api); } diff --git a/js/src/contracts/githubhint.js b/js/src/contracts/githubhint.js new file mode 100644 index 000000000..47d7eca6e --- /dev/null +++ b/js/src/contracts/githubhint.js @@ -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 . + +export default class GithubHint { + constructor (api, registry) { + this._api = api; + this._registry = registry; + + this.getInstance(); + } + + getContract () { + return this._registry.getContract('githubhint'); + } + + getInstance () { + return this.getContract().instance; + } +} diff --git a/js/src/contracts/registry.js b/js/src/contracts/registry.js index 9853c0df9..d52b20718 100644 --- a/js/src/contracts/registry.js +++ b/js/src/contracts/registry.js @@ -42,7 +42,7 @@ export default class Registry { }); } - getContractInstance (_name) { + getContract (_name) { const name = _name.toLowerCase(); return new Promise((resolve, reject) => { @@ -54,13 +54,19 @@ export default class Registry { this .lookupAddress(name) .then((address) => { - this._contracts[name] = this._api.newContract(abis[name], address).instance; + this._contracts[name] = this._api.newContract(abis[name], address); resolve(this._contracts[name]); }) .catch(reject); }); } + getContractInstance (_name) { + return this + .getContract(_name) + .then((contract) => contract.instance); + } + lookupAddress (_name) { const name = _name.toLowerCase(); const sha3 = this._api.util.sha3(name); diff --git a/js/src/contracts/tokenreg.js b/js/src/contracts/tokenreg.js index 78bd5e4d9..5e317880b 100644 --- a/js/src/contracts/tokenreg.js +++ b/js/src/contracts/tokenreg.js @@ -22,8 +22,12 @@ export default class TokenReg { this.getInstance(); } + getContract () { + return this._registry.getContract('tokenreg'); + } + getInstance () { - return this._registry.getContractInstance('tokenreg'); + return this.getContract().instance; } tokenCount () { diff --git a/js/src/dapps/tokenreg/Accounts/actions.js b/js/src/dapps/tokenreg/Accounts/actions.js index a310baf7d..5f92280be 100644 --- a/js/src/dapps/tokenreg/Accounts/actions.js +++ b/js/src/dapps/tokenreg/Accounts/actions.js @@ -46,8 +46,6 @@ export const loadAccounts = () => (dispatch) => { address })); - console.log('accounts', accountsList); - dispatch(setAccounts(accountsList)); dispatch(setAccountsInfo(accountsInfo)); dispatch(setSelectedAccount(accountsList[0].address)); diff --git a/js/src/dapps/tokenreg/Actions/Query/query.js b/js/src/dapps/tokenreg/Actions/Query/query.js index 16d13f2f0..5a3c7d5f6 100644 --- a/js/src/dapps/tokenreg/Actions/Query/query.js +++ b/js/src/dapps/tokenreg/Actions/Query/query.js @@ -42,12 +42,9 @@ export default class QueryAction extends Component { onClose: PropTypes.func.isRequired, handleQueryToken: PropTypes.func.isRequired, - handleQueryMetaLookup: PropTypes.func.isRequired, data: PropTypes.object, - notFound: PropTypes.bool, - metaLoading: PropTypes.bool, - metaData: PropTypes.object + notFound: PropTypes.bool } state = initState; @@ -131,11 +128,8 @@ export default class QueryAction extends Component { return ( + tla={ data.tla } + /> ); } diff --git a/js/src/dapps/tokenreg/Actions/actions.js b/js/src/dapps/tokenreg/Actions/actions.js index 1c6703f77..df5b41e6b 100644 --- a/js/src/dapps/tokenreg/Actions/actions.js +++ b/js/src/dapps/tokenreg/Actions/actions.js @@ -16,8 +16,6 @@ import { getTokenTotalSupply } from '../utils'; -const { sha3, bytesToHex } = window.parity.api.util; - export const SET_REGISTER_SENDING = 'SET_REGISTER_SENDING'; export const setRegisterSending = (isSending) => ({ type: SET_REGISTER_SENDING, @@ -41,8 +39,6 @@ export const registerCompleted = () => ({ }); export const registerToken = (tokenData) => (dispatch, getState) => { - console.log('registering token', tokenData); - const state = getState(); const contractInstance = state.status.contract.instance; const fee = state.status.contract.fee; @@ -83,8 +79,6 @@ export const registerToken = (tokenData) => (dispatch, getState) => { }) .then((gasEstimate) => { options.gas = gasEstimate.mul(1.2).toFixed(0); - console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`); - return contractInstance.register.postTransaction(options, values); }) .then((result) => { @@ -183,34 +177,3 @@ export const queryToken = (key, query) => (dispatch, getState) => { dispatch(setQueryLoading(false)); }); }; - -export const queryTokenMeta = (id, query) => (dispatch, getState) => { - console.log('loading token meta', query); - - const state = getState(); - const contractInstance = state.status.contract.instance; - - const key = sha3(query); - - const startDate = Date.now(); - dispatch(setQueryMetaLoading(true)); - - contractInstance - .meta - .call({}, [ id, key ]) - .then((value) => { - const meta = { - key, query, - value: value.find(v => v !== 0) ? bytesToHex(value) : null - }; - - dispatch(setQueryMeta(meta)); - - setTimeout(() => { - dispatch(setQueryMetaLoading(false)); - }, 500 - (Date.now() - startDate)); - }) - .catch((e) => { - console.error('load meta query error', e); - }); -}; diff --git a/js/src/dapps/tokenreg/Actions/component.js b/js/src/dapps/tokenreg/Actions/component.js index 3e7ef0d64..43acc27ab 100644 --- a/js/src/dapps/tokenreg/Actions/component.js +++ b/js/src/dapps/tokenreg/Actions/component.js @@ -37,7 +37,6 @@ export default class Actions extends Component { handleQueryToken: PropTypes.func.isRequired, handleQueryClose: PropTypes.func.isRequired, - handleQueryMetaLookup: PropTypes.func.isRequired, query: PropTypes.object.isRequired }; @@ -82,7 +81,6 @@ export default class Actions extends Component { show={ this.state.show[ QUERY_ACTION ] } onClose={ this.onQueryClose } handleQueryToken={ this.props.handleQueryToken } - handleQueryMetaLookup={ this.props.handleQueryMetaLookup } { ...this.props.query } /> ); diff --git a/js/src/dapps/tokenreg/Actions/container.js b/js/src/dapps/tokenreg/Actions/container.js index 2c3773122..1d3d8fe31 100644 --- a/js/src/dapps/tokenreg/Actions/container.js +++ b/js/src/dapps/tokenreg/Actions/container.js @@ -19,7 +19,7 @@ import { connect } from 'react-redux'; import Actions from './component'; -import { registerToken, registerReset, queryToken, queryReset, queryTokenMeta } from './actions'; +import { registerToken, registerReset, queryToken, queryReset } from './actions'; class TokensContainer extends Component { @@ -49,9 +49,6 @@ const mapDispatchToProps = (dispatch) => { }, handleQueryClose: () => { dispatch(queryReset()); - }, - handleQueryMetaLookup: (id, query) => { - dispatch(queryTokenMeta(id, query)); } }; }; diff --git a/js/src/dapps/tokenreg/Application/application.css b/js/src/dapps/tokenreg/Application/application.css index 033147ae3..94ca6302e 100644 --- a/js/src/dapps/tokenreg/Application/application.css +++ b/js/src/dapps/tokenreg/Application/application.css @@ -19,6 +19,7 @@ display: flex; flex-direction: column; align-items: center; + padding-bottom: 10em; } .warning { diff --git a/js/src/dapps/tokenreg/Status/actions.js b/js/src/dapps/tokenreg/Status/actions.js index e9e217d6a..b07949a28 100644 --- a/js/src/dapps/tokenreg/Status/actions.js +++ b/js/src/dapps/tokenreg/Status/actions.js @@ -14,11 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { - registry as registryAbi, - tokenreg as tokenregAbi, - githubhint as githubhintAbi -} from '../../../contracts/abi'; +import Contracts from '../../../contracts'; import { loadToken, setTokenPending, deleteToken, setTokenData } from '../Tokens/actions'; @@ -34,43 +30,31 @@ export const FIND_CONTRACT = 'FIND_CONTRACT'; export const loadContract = () => (dispatch) => { dispatch(setLoading(true)); - api.parity - .registryAddress() - .then((registryAddress) => { - console.log(`registry found at ${registryAddress}`); - const registry = api.newContract(registryAbi, registryAddress).instance; - - return Promise.all([ - registry.getAddress.call({}, [api.util.sha3('tokenreg'), 'A']), - registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']) - ]); - }) - .then(([ tokenregAddress, githubhintAddress ]) => { - console.log(`tokenreg was found at ${tokenregAddress}`); - - const tokenregContract = api - .newContract(tokenregAbi, tokenregAddress); - - const githubhintContract = api - .newContract(githubhintAbi, githubhintAddress); + const { tokenReg, githubHint } = new Contracts(api); + return Promise + .all([ + tokenReg.getContract(), + githubHint.getContract() + ]) + .then(([ tokenRegContract, githubHintContract ]) => { dispatch(setContractDetails({ - address: tokenregAddress, - instance: tokenregContract.instance, - raw: tokenregContract + address: tokenRegContract.address, + instance: tokenRegContract.instance, + raw: tokenRegContract })); dispatch(setGithubhintDetails({ - address: githubhintAddress, - instance: githubhintContract.instance, - raw: githubhintContract + address: githubHintContract.address, + instance: githubHintContract.instance, + raw: githubHintContract })); dispatch(loadContractDetails()); dispatch(subscribeEvents()); }) .catch((error) => { - console.error('loadContract error', error); + throw error; }); }; @@ -78,7 +62,7 @@ export const LOAD_CONTRACT_DETAILS = 'LOAD_CONTRACT_DETAILS'; export const loadContractDetails = () => (dispatch, getState) => { const state = getState(); - const instance = state.status.contract.instance; + const { instance } = state.status.contract; Promise .all([ @@ -87,8 +71,6 @@ export const loadContractDetails = () => (dispatch, getState) => { instance.fee.call() ]) .then(([accounts, owner, fee]) => { - console.log(`owner as ${owner}, fee set at ${fee.toFormat()}`); - const isOwner = accounts.filter(a => a === owner).length > 0; dispatch(setContractDetails({ @@ -119,14 +101,14 @@ export const setGithubhintDetails = (details) => ({ export const subscribeEvents = () => (dispatch, getState) => { const state = getState(); - const contract = state.status.contract.raw; + const { raw } = state.status.contract; const previousSubscriptionId = state.status.subscriptionId; if (previousSubscriptionId) { - contract.unsubscribe(previousSubscriptionId); + raw.unsubscribe(previousSubscriptionId); } - contract + raw .subscribe(null, { fromBlock: 'latest', toBlock: 'pending', @@ -187,7 +169,7 @@ export const subscribeEvents = () => (dispatch, getState) => { )); } - console.log('new log event', log); + console.warn('unknown log event', log); }); }) .then((subscriptionId) => { diff --git a/js/src/dapps/tokenreg/Status/reducer.js b/js/src/dapps/tokenreg/Status/reducer.js index 357cb2244..aee16fbe7 100644 --- a/js/src/dapps/tokenreg/Status/reducer.js +++ b/js/src/dapps/tokenreg/Status/reducer.js @@ -27,15 +27,13 @@ const initialState = { contract: { address: null, instance: null, - raw: null, owner: null, isOwner: false, fee: null }, githubhint: { address: null, - instance: null, - raw: null + instance: null } }; diff --git a/js/src/dapps/tokenreg/Tokens/Token/index.js b/js/src/dapps/tokenreg/Tokens/Token/index.js index 4b822b4bd..30ad8896f 100644 --- a/js/src/dapps/tokenreg/Tokens/Token/index.js +++ b/js/src/dapps/tokenreg/Tokens/Token/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './token'; +export default from './tokenContainer'; diff --git a/js/src/dapps/tokenreg/Tokens/Token/token.js b/js/src/dapps/tokenreg/Tokens/Token/token.js index 660b21b97..be14cec84 100644 --- a/js/src/dapps/tokenreg/Tokens/Token/token.js +++ b/js/src/dapps/tokenreg/Tokens/Token/token.js @@ -57,15 +57,28 @@ export default class Token extends Component { isLoading: PropTypes.bool, isPending: PropTypes.bool, isTokenOwner: PropTypes.bool.isRequired, - isContractOwner: PropTypes.bool.isRequired, + isContractOwner: PropTypes.bool, fullWidth: PropTypes.bool }; - state = { - metaKeyIndex: 0 + static defaultProps = { + isContractOwner: false }; + state = { + metaKeyIndex: 0, + showMeta: false + }; + + shouldComponentUpdate (nextProps) { + if (nextProps.isLoading && this.props.isLoading) { + return false; + } + + return true; + } + render () { const { isLoading, fullWidth } = this.props; @@ -237,7 +250,12 @@ export default class Token extends Component { } renderMeta (meta) { - const isMetaLoading = this.props.isMetaLoading; + const { isMetaLoading } = this.props; + const { showMeta } = this.state; + + if (!showMeta) { + return null; + } if (isMetaLoading) { return (
@@ -331,6 +349,7 @@ export default class Token extends Component { const key = metaDataKeys[keyIndex].value; const index = this.props.index; + this.setState({ showMeta: true }); this.props.handleMetaLookup(index, key); } diff --git a/js/src/dapps/tokenreg/Tokens/Token/tokenContainer.js b/js/src/dapps/tokenreg/Tokens/Token/tokenContainer.js new file mode 100644 index 000000000..7351da524 --- /dev/null +++ b/js/src/dapps/tokenreg/Tokens/Token/tokenContainer.js @@ -0,0 +1,73 @@ +// 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 { connect } from 'react-redux'; + +import Token from './token'; + +import { queryTokenMeta, unregisterToken, addTokenMeta } from '../actions'; + +class TokenContainer extends Component { + static propTypes = { + handleMetaLookup: PropTypes.func.isRequired, + handleUnregister: PropTypes.func.isRequired, + handleAddMeta: PropTypes.func.isRequired, + + tla: PropTypes.string.isRequired + }; + + render () { + return ( + + ); + } +} + +const mapStateToProps = (_, initProps) => { + const { tla } = initProps; + + return (state) => { + const { isOwner } = state.status.contract; + const { tokens } = state.tokens; + const token = tokens.find((t) => t.tla === tla); + + return { ...token, isContractOwner: isOwner }; + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + handleMetaLookup: (index, query) => { + dispatch(queryTokenMeta(index, query)); + }, + + handleUnregister: (index) => { + dispatch(unregisterToken(index)); + }, + + handleAddMeta: (index, key, value) => { + dispatch(addTokenMeta(index, key, value)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TokenContainer); diff --git a/js/src/dapps/tokenreg/Tokens/actions.js b/js/src/dapps/tokenreg/Tokens/actions.js index bd4413163..d1e13c1dc 100644 --- a/js/src/dapps/tokenreg/Tokens/actions.js +++ b/js/src/dapps/tokenreg/Tokens/actions.js @@ -67,8 +67,6 @@ export const deleteToken = (index) => ({ }); export const loadTokens = () => (dispatch, getState) => { - console.log('loading tokens...'); - const state = getState(); const contractInstance = state.status.contract.instance; @@ -79,7 +77,6 @@ export const loadTokens = () => (dispatch, getState) => { .call() .then((count) => { const tokenCount = parseInt(count); - console.log(`token count: ${tokenCount}`); dispatch(setTokenCount(tokenCount)); for (let i = 0; i < tokenCount; i++) { @@ -94,8 +91,6 @@ export const loadTokens = () => (dispatch, getState) => { }; export const loadToken = (index) => (dispatch, getState) => { - console.log('loading token', index); - const state = getState(); const contractInstance = state.status.contract.instance; @@ -144,7 +139,7 @@ export const loadToken = (index) => (dispatch, getState) => { } data.totalSupply = data.totalSupply.toNumber(); - console.log(`token loaded: #${index}`, data); + dispatch(setTokenData(index, data)); dispatch(setTokenLoading(index, false)); }) @@ -159,8 +154,6 @@ export const loadToken = (index) => (dispatch, getState) => { }; export const queryTokenMeta = (index, query) => (dispatch, getState) => { - console.log('loading token meta', index, query); - const state = getState(); const contractInstance = state.status.contract.instance; @@ -176,7 +169,6 @@ export const queryTokenMeta = (index, query) => (dispatch, getState) => { value: value.find(v => v !== 0) ? bytesToHex(value) : null }; - console.log(`token meta loaded: #${index}`, value); dispatch(setTokenMeta(index, meta)); setTimeout(() => { @@ -189,8 +181,6 @@ export const queryTokenMeta = (index, query) => (dispatch, getState) => { }; export const addTokenMeta = (index, key, value) => (dispatch, getState) => { - console.log('add token meta', index, key, value); - const state = getState(); const contractInstance = state.status.contract.instance; const token = state.tokens.tokens.find(t => t.index === index); @@ -203,8 +193,6 @@ export const addTokenMeta = (index, key, value) => (dispatch, getState) => { .estimateGas(options, values) .then((gasEstimate) => { options.gas = gasEstimate.mul(1.2).toFixed(0); - console.log(`addTokenMeta: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`); - return contractInstance.setMeta.postTransaction(options, values); }) .catch((e) => { @@ -213,8 +201,6 @@ export const addTokenMeta = (index, key, value) => (dispatch, getState) => { }; export const addGithubhintURL = (from, key, url) => (dispatch, getState) => { - console.log('add githubhint url', key, url); - const state = getState(); const contractInstance = state.status.githubhint.instance; @@ -227,8 +213,6 @@ export const addGithubhintURL = (from, key, url) => (dispatch, getState) => { .estimateGas(options, values) .then((gasEstimate) => { options.gas = gasEstimate.mul(1.2).toFixed(0); - console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`); - return contractInstance.hintURL.postTransaction(options, values); }) .catch((e) => { @@ -237,8 +221,6 @@ export const addGithubhintURL = (from, key, url) => (dispatch, getState) => { }; export const unregisterToken = (index) => (dispatch, getState) => { - console.log('unregistering token', index); - const { contract } = getState().status; const { instance, owner } = contract; @@ -252,8 +234,6 @@ export const unregisterToken = (index) => (dispatch, getState) => { .estimateGas(options, values) .then((gasEstimate) => { options.gas = gasEstimate.mul(1.2).toFixed(0); - console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`); - return instance.unregister.postTransaction(options, values); }) .catch((e) => { diff --git a/js/src/dapps/tokenreg/Tokens/container.js b/js/src/dapps/tokenreg/Tokens/container.js index b6171c2cc..33b2de659 100644 --- a/js/src/dapps/tokenreg/Tokens/container.js +++ b/js/src/dapps/tokenreg/Tokens/container.js @@ -19,16 +19,13 @@ import { connect } from 'react-redux'; import Tokens from './tokens'; -import { loadTokens, queryTokenMeta, unregisterToken, addTokenMeta } from './actions'; +import { loadTokens } from './actions'; class TokensContainer extends Component { static propTypes = { - isOwner: PropTypes.bool, isLoading: PropTypes.bool, tokens: PropTypes.array, - tokenCount: PropTypes.number, - onLoadTokens: PropTypes.func, - accounts: PropTypes.array + onLoadTokens: PropTypes.func }; componentDidMount () { @@ -36,7 +33,6 @@ class TokensContainer extends Component { } render () { - console.log(this.props); return ( { - const { list } = state.accounts; - const { isLoading, tokens, tokenCount } = state.tokens; + const { isLoading, tokens } = state.tokens; - const { isOwner } = state.status.contract; + const filteredTokens = tokens + .filter((token) => token && token.tla) + .map((token) => ({ tla: token.tla, owner: token.owner })); - return { isLoading, tokens, tokenCount, isOwner, accounts: list }; + return { isLoading, tokens: filteredTokens }; }; const mapDispatchToProps = (dispatch) => { return { onLoadTokens: () => { dispatch(loadTokens()); - }, - - handleMetaLookup: (index, query) => { - dispatch(queryTokenMeta(index, query)); - }, - - handleUnregister: (index) => { - dispatch(unregisterToken(index)); - }, - - handleAddMeta: (index, key, value) => { - dispatch(addTokenMeta(index, key, value)); } }; }; diff --git a/js/src/dapps/tokenreg/Tokens/tokens.js b/js/src/dapps/tokenreg/Tokens/tokens.js index 43766a8a8..48bc88a74 100644 --- a/js/src/dapps/tokenreg/Tokens/tokens.js +++ b/js/src/dapps/tokenreg/Tokens/tokens.js @@ -23,13 +23,8 @@ import styles from './tokens.css'; export default class Tokens extends Component { static propTypes = { - handleAddMeta: PropTypes.func.isRequired, - handleUnregister: PropTypes.func.isRequired, - handleMetaLookup: PropTypes.func.isRequired, - isOwner: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, - tokens: PropTypes.array, - accounts: PropTypes.array + tokens: PropTypes.array }; render () { @@ -45,24 +40,12 @@ export default class Tokens extends Component { } renderTokens (tokens) { - const { accounts, isOwner } = this.props; - - return tokens.map((token, index) => { - if (!token || !token.tla) { - return null; - } - - const isTokenOwner = !!accounts.find((account) => account.address === token.owner); - + return tokens.map((token) => { return ( + key={ token.tla } + tla={ token.tla } + /> ); }); } From a7574a1108eb547989a006eb4bee1783d3ebd1fc Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 16 Nov 2016 16:12:55 +0000 Subject: [PATCH 10/15] [ci skip] js-precompiled 20161116-161024 --- Cargo.lock | 2 +- ethcore/res/ethereum/tests | 2 +- js/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af4103bd5..52906c509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#985a6d9cf9aa4621172fcb8e4bf6955f33d5e2a3" +source = "git+https://github.com/ethcore/js-precompiled.git#5e3b9629692c550811b228d68ca99d1461a4f6cb" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/res/ethereum/tests b/ethcore/res/ethereum/tests index 9028c4801..97066e40c 160000 --- a/ethcore/res/ethereum/tests +++ b/ethcore/res/ethereum/tests @@ -1 +1 @@ -Subproject commit 9028c4801fd39fbb71a9796979182549a24e81c8 +Subproject commit 97066e40ccd061f727deb5cd860e4d9135aa2551 diff --git a/js/package.json b/js/package.json index bf8db47ab..59b75c591 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.48", + "version": "0.2.49", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From 18f570327ef788d3a85cd3f8b8d335324ec087e8 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 16 Nov 2016 18:03:59 +0100 Subject: [PATCH 11/15] Enhanced MethodDecoding in Transactions list (#3454) * Better MethodDecoding UI // No empty params #3452 * Display input if ASCII in transactions #3434 * Fixes UI for #3454 * Better MethodDecoding contract detection #3454 --- js/src/api/util/format.js | 12 + js/src/api/util/index.js | 3 +- js/src/ui/Form/Input/input.js | 5 +- js/src/ui/Form/InputAddress/inputAddress.css | 1 + js/src/ui/Form/InputAddress/inputAddress.js | 1 + js/src/ui/MethodDecoding/methodDecoding.css | 12 +- js/src/ui/MethodDecoding/methodDecoding.js | 230 ++++++++++++++----- 7 files changed, 206 insertions(+), 58 deletions(-) diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js index 1b68e1138..198e456ee 100644 --- a/js/src/api/util/format.js +++ b/js/src/api/util/format.js @@ -17,3 +17,15 @@ export function bytesToHex (bytes) { return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join(''); } + +export function hex2Ascii (_hex) { + const hex = /^(?:0x)?(.*)$/.exec(_hex.toString())[1]; + + let str = ''; + + for (let i = 0; i < hex.length; i += 2) { + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + } + + return str; +} diff --git a/js/src/api/util/index.js b/js/src/api/util/index.js index fb0f79076..55cf008c5 100644 --- a/js/src/api/util/index.js +++ b/js/src/api/util/index.js @@ -16,7 +16,7 @@ import { isAddress as isAddressValid, toChecksumAddress } from '../../abi/util/address'; import { decodeCallData, decodeMethodInput, methodToAbi } from './decode'; -import { bytesToHex } from './format'; +import { bytesToHex, hex2Ascii } from './format'; import { fromWei, toWei } from './wei'; import { sha3 } from './sha3'; import { isArray, isFunction, isHex, isInstanceOf, isString } from './types'; @@ -30,6 +30,7 @@ export default { isInstanceOf, isString, bytesToHex, + hex2Ascii, createIdentityImg, decodeCallData, decodeMethodInput, diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index 47d0dde90..4dee227c6 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -144,7 +144,7 @@ export default class Input extends Component { } renderCopyButton () { - const { allowCopy, hideUnderline, label, hint, floatCopy } = this.props; + const { allowCopy, label, hint, floatCopy } = this.props; const { value } = this.state; if (!allowCopy) { @@ -159,7 +159,7 @@ export default class Input extends Component { ? allowCopy : value; - if (hideUnderline && !label) { + if (!label) { style.marginBottom = 2; } else if (label && !hint) { style.marginBottom = 4; @@ -182,6 +182,7 @@ export default class Input extends Component { } onChange = (event, value) => { + event.persist(); this.setValue(value, () => { this.props.onChange && this.props.onChange(event, value); }); diff --git a/js/src/ui/Form/InputAddress/inputAddress.css b/js/src/ui/Form/InputAddress/inputAddress.css index cf41ab1f2..6a32cd8ea 100644 --- a/js/src/ui/Form/InputAddress/inputAddress.css +++ b/js/src/ui/Form/InputAddress/inputAddress.css @@ -20,6 +20,7 @@ .input input { padding-left: 48px !important; + box-sizing: border-box; } .inputEmpty input { diff --git a/js/src/ui/Form/InputAddress/inputAddress.js b/js/src/ui/Form/InputAddress/inputAddress.js index fd205e941..a3f61739f 100644 --- a/js/src/ui/Form/InputAddress/inputAddress.js +++ b/js/src/ui/Form/InputAddress/inputAddress.js @@ -76,6 +76,7 @@ class InputAddress extends Component { } const classes = [disabled ? styles.iconDisabled : styles.icon]; + if (!label) { classes.push(styles.noLabel); } diff --git a/js/src/ui/MethodDecoding/methodDecoding.css b/js/src/ui/MethodDecoding/methodDecoding.css index 175ed965e..2de91cf9b 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.css +++ b/js/src/ui/MethodDecoding/methodDecoding.css @@ -18,6 +18,12 @@ .container { } +.loading { + display: flex; + align-items: center; + justify-content: center; +} + .details, .gasDetails { color: #aaa; @@ -46,7 +52,7 @@ .highlight { } -.inputs { +.inputs, .addressContainer { padding-left: 2em; } @@ -73,3 +79,7 @@ margin-bottom: -10px; margin-right: 0.5em; } + +.inputData { + word-break: break-all; +} diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 648ff271b..d446a19b4 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -18,14 +18,14 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import CircularProgress from 'material-ui/CircularProgress'; import Contracts from '../../contracts'; -import IdentityIcon from '../IdentityIcon'; -import IdentityName from '../IdentityName'; import { Input, InputAddress } from '../Form'; import styles from './methodDecoding.css'; +const ASCII_INPUT = /^[a-z0-9\s,?;.:/!()-_@'"#]+$/i; const CONTRACT_CREATE = '0x60606040'; const TOKEN_METHODS = { '0xa9059cbb': 'transfer(to,value)' @@ -53,20 +53,36 @@ class MethodDecoding extends Component { token: null, isContract: false, isDeploy: false, - isReceived: false + isReceived: false, + isLoading: true } componentWillMount () { - this.lookup(); + const lookupResult = this.lookup(); + + if (typeof lookupResult === 'object' && typeof lookupResult.then === 'function') { + lookupResult.then(() => this.setState({ isLoading: false })); + } else { + this.setState({ isLoading: false }); + } } render () { const { transaction } = this.props; + const { isLoading } = this.state; if (!transaction) { return null; } + if (isLoading) { + return ( +
+ +
+ ); + } + return (
{ this.renderAction() } @@ -82,26 +98,33 @@ class MethodDecoding extends Component { return (
- { historic ? 'Provided' : 'Provides' } { gas.toFormat(0) } gas ({ gasPrice.div(1000000).toFormat(0) }M/ETH) for a total transaction value of { this.renderEtherValue(gasValue) } + { historic ? 'Provided' : 'Provides' } + + { gas.toFormat(0) } gas ({ gasPrice.div(1000000).toFormat(0) }M/ETH) + + for a total transaction value of + { this.renderEtherValue(gasValue) }
); } renderAction () { - const { methodName, methodInputs, methodSignature, token, isDeploy, isReceived } = this.state; + const { methodName, methodInputs, methodSignature, token, isDeploy, isReceived, isContract } = this.state; if (isDeploy) { return this.renderDeploy(); } - if (methodSignature) { + if (isContract && methodSignature) { if (token && TOKEN_METHODS[methodSignature] && methodInputs) { return this.renderTokenAction(); } - return methodName - ? this.renderSignatureMethod() - : this.renderUnknownMethod(); + if (methodName) { + return this.renderSignatureMethod(); + } + + return this.renderUnknownMethod(); } return isReceived @@ -109,6 +132,28 @@ class MethodDecoding extends Component { : this.renderValueTransfer(); } + renderInputValue () { + const { api } = this.context; + const { transaction } = this.props; + + if (!/^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(transaction.input)) { + return null; + } + + const ascii = api.util.hex2Ascii(transaction.input); + + const text = ASCII_INPUT.test(ascii) + ? ascii + : transaction.input; + + return ( +
+ with the input   + { text } +
+ ); + } + renderTokenAction () { const { historic } = this.props; const { methodSignature, methodInputs } = this.state; @@ -120,7 +165,15 @@ class MethodDecoding extends Component { default: return (
- { historic ? 'Transferred' : 'Will transfer' } { this.renderTokenValue(value.value) } to { this.renderAddressName(address) }. +
+ { historic ? 'Transferred' : 'Will transfer' } + + { this.renderTokenValue(value.value) } + + to +
+ + { this.renderAddressName(address) }
); } @@ -139,7 +192,11 @@ class MethodDecoding extends Component { return (
- Deployed a contract at address { this.renderAddressName(transaction.creates, false) } +
+ Deployed a contract at address +
+ + { this.renderAddressName(transaction.creates, false) }
); } @@ -150,7 +207,16 @@ class MethodDecoding extends Component { return (
- { historic ? 'Received' : 'Will receive' } { this.renderEtherValue(transaction.value) } from { isContract ? 'the contract' : '' } { this.renderAddressName(transaction.from) } +
+ { historic ? 'Received' : 'Will receive' } + + { this.renderEtherValue(transaction.value) } + + from { isContract ? 'the contract' : '' } +
+ + { this.renderAddressName(transaction.from) } + { this.renderInputValue() }
); } @@ -161,19 +227,44 @@ class MethodDecoding extends Component { return (
- { historic ? 'Transferred' : 'Will transfer' } { this.renderEtherValue(transaction.value) } to { isContract ? 'the contract' : '' } { this.renderAddressName(transaction.to) } +
+ { historic ? 'Transferred' : 'Will transfer' } + + { this.renderEtherValue(transaction.value) } + + to { isContract ? 'the contract' : '' } +
+ + { this.renderAddressName(transaction.to) } + { this.renderInputValue() }
); } renderSignatureMethod () { const { historic, transaction } = this.props; - const { methodName } = this.state; + const { methodName, methodInputs } = this.state; return (
- { historic ? 'Executed' : 'Will execute' } the { methodName } function on the contract { this.renderAddressName(transaction.to) }, transferring { this.renderEtherValue(transaction.value) }, passing the following parameters: +
+ { historic ? 'Executed' : 'Will execute' } the + { methodName } + function on the contract +
+ + { this.renderAddressName(transaction.to) } + +
+ transferring + + { this.renderEtherValue(transaction.value) } + + + { methodInputs.length ? ', passing the following parameters:' : '.' } + +
{ this.renderInputs() } @@ -187,7 +278,21 @@ class MethodDecoding extends Component { return (
- { historic ? 'Executed' : 'Will execute' } an unknown/unregistered method on the contract { this.renderAddressName(transaction.to) }, transferring { this.renderEtherValue(transaction.value) }. +
+ { historic ? 'Executed' : 'Will execute' } + an unknown/unregistered + method on the contract +
+ + { this.renderAddressName(transaction.to) } + +
+ transferring + + { this.renderEtherValue(transaction.value) } + + . +
); } @@ -239,7 +344,7 @@ class MethodDecoding extends Component { return ( - { value.div(token.format).toFormat(5) }{ token.tag } + { value.div(token.format).toFormat(5) } { token.tag } ); } @@ -250,17 +355,21 @@ class MethodDecoding extends Component { return ( - { ether.toFormat(5) }ETH + { ether.toFormat(5) } ETH ); } renderAddressName (address, withName = true) { return ( - - - { withName ? : address } - +
+ +
); } @@ -284,44 +393,57 @@ class MethodDecoding extends Component { return; } - const { signature, paramdata } = api.util.decodeCallData(transaction.input); - this.setState({ methodSignature: signature, methodParams: paramdata }); - - if (!signature || signature === CONTRACT_CREATE || transaction.creates) { - this.setState({ isDeploy: true }); + if (contractAddress === '0x') { return; } - Promise - .all([ - api.eth.getCode(contractAddress), - Contracts.get().signatureReg.lookup(signature) - ]) - .then(([bytecode, method]) => { - let methodInputs = null; - let methodName = null; + return api.eth + .getCode(contractAddress || transaction.creates) + .then((bytecode) => { + const isContract = bytecode && /^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(bytecode); - if (method && method.length) { - const { methodParams } = this.state; - const abi = api.util.methodToAbi(method); + this.setState({ isContract }); - methodName = abi.name; - methodInputs = api.util - .decodeMethodInput(abi, methodParams) - .map((value, index) => { - const type = abi.inputs[index].type; - - return { type, value }; - }); + if (!isContract) { + return; } - this.setState({ - method, - methodName, - methodInputs, - bytecode, - isContract: bytecode && bytecode !== '0x' - }); + const { signature, paramdata } = api.util.decodeCallData(transaction.input); + this.setState({ methodSignature: signature, methodParams: paramdata }); + + if (!signature || signature === CONTRACT_CREATE || transaction.creates) { + this.setState({ isDeploy: true }); + return; + } + + return Contracts.get() + .signatureReg + .lookup(signature) + .then((method) => { + let methodInputs = null; + let methodName = null; + + if (method && method.length) { + const { methodParams } = this.state; + const abi = api.util.methodToAbi(method); + + methodName = abi.name; + methodInputs = api.util + .decodeMethodInput(abi, methodParams) + .map((value, index) => { + const type = abi.inputs[index].type; + + return { type, value }; + }); + } + + this.setState({ + method, + methodName, + methodInputs, + bytecode + }); + }); }) .catch((error) => { console.warn('lookup', error); From 7144da5d7ee5bae8a7be467b8ad2b2b71dcb9913 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 16 Nov 2016 18:55:53 +0100 Subject: [PATCH 12/15] Handle Signer Rejection // Real Custom Errors #3153 --- js/package.json | 1 + js/src/api/transport/error.js | 53 +++++++++++++++++++ js/src/api/transport/http/http.js | 4 +- js/src/api/transport/index.js | 1 + js/src/api/transport/ws/ws.js | 5 +- .../modals/DeployContract/deployContract.js | 34 +++++++++--- .../modals/ExecuteContract/executeContract.js | 29 ++++++++-- js/src/modals/Transfer/transfer.js | 41 +++++++++++--- 8 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 js/src/api/transport/error.js diff --git a/js/package.json b/js/package.json index 59b75c591..c90742c05 100644 --- a/js/package.json +++ b/js/package.json @@ -122,6 +122,7 @@ "brace": "^0.9.0", "bytes": "^2.4.0", "chart.js": "^2.3.0", + "es6-error": "^4.0.0", "es6-promise": "^3.2.1", "ethereumjs-tx": "^1.1.2", "file-saver": "^1.3.3", diff --git a/js/src/api/transport/error.js b/js/src/api/transport/error.js new file mode 100644 index 000000000..341839f69 --- /dev/null +++ b/js/src/api/transport/error.js @@ -0,0 +1,53 @@ +// 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 ExtendableError from 'es6-error'; + +export const ERROR_CODES = { + UNSUPPORTED_REQUEST: -32000, + NO_WORK: -32001, + NO_AUTHOR: -32002, + NO_NEW_WORK: -32003, + NOT_ENOUGH_DATA: -32006, + UNKNOWN_ERROR: -32009, + TRANSACTION_ERROR: -32010, + EXECUTION_ERROR: -32015, + ACCOUNT_LOCKED: -32020, + PASSWORD_INVALID: -32021, + ACCOUNT_ERROR: -32023, + SIGNER_DISABLED: -32030, + DAPPS_DISABLED: -32031, + NETWORK_DISABLED: -32035, + REQUEST_REJECTED: -32040, + REQUEST_REJECTED_LIMIT: -32041, + REQUEST_NOT_FOUND: -32042, + COMPILATION_ERROR: -32050, + ENCRYPTION_ERROR: -32055, + FETCH_ERROR: -32060 +}; + +export default class TransportError extends ExtendableError { + constructor (method, code, message) { + const m = `${method}: ${code}: ${message}`; + super(m); + + this.code = code; + this.type = Object.keys(ERROR_CODES).find((k) => ERROR_CODES[k] === code) || ''; + + this.method = method; + this.text = message; + } +} diff --git a/js/src/api/transport/http/http.js b/js/src/api/transport/http/http.js index 8ea59f0fb..591b9a627 100644 --- a/js/src/api/transport/http/http.js +++ b/js/src/api/transport/http/http.js @@ -16,6 +16,7 @@ import { Logging } from '../../subscriptions'; import JsonRpcBase from '../jsonRpcBase'; +import TransportError from '../error'; /* global fetch */ export default class Http extends JsonRpcBase { @@ -73,7 +74,8 @@ export default class Http extends JsonRpcBase { this.error(JSON.stringify(response)); console.error(`${method}(${JSON.stringify(params)}): ${response.error.code}: ${response.error.message}`); - throw new Error(`${method}: ${response.error.code}: ${response.error.message}`); + const error = new TransportError(method, response.error.code, response.error.message); + throw error; } this.log(JSON.stringify(response)); diff --git a/js/src/api/transport/index.js b/js/src/api/transport/index.js index 8f67fba4d..84fbac826 100644 --- a/js/src/api/transport/index.js +++ b/js/src/api/transport/index.js @@ -16,3 +16,4 @@ export Http from './http'; export Ws from './ws'; +export TransportError from './error.js'; diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index d608426b0..1cb1fb1c4 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -18,6 +18,7 @@ import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase import { Logging } from '../../subscriptions'; import JsonRpcBase from '../jsonRpcBase'; +import TransportError from '../error'; /* global WebSocket */ export default class Ws extends JsonRpcBase { @@ -109,7 +110,9 @@ export default class Ws extends JsonRpcBase { console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); - reject(new Error(`${method}: ${result.error.code}: ${result.error.message}`)); + const error = new TransportError(method, result.error.code, result.error.message); + reject(error); + delete this._messages[result.id]; return; } diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index a99b49412..996948092 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -26,6 +26,8 @@ import ErrorStep from './ErrorStep'; import styles from './deployContract.css'; +import { ERROR_CODES } from '../../api/transport/error'; + const steps = ['contract details', 'deployment', 'completed']; export default class DeployContract extends Component { @@ -63,7 +65,8 @@ export default class DeployContract extends Component { params: [], paramsError: [], step: 0, - deployError: null + deployError: null, + rejected: false } componentWillMount () { @@ -92,15 +95,20 @@ export default class DeployContract extends Component { } render () { - const { step, deployError } = this.state; + const { step, deployError, rejected } = this.state; + + const realSteps = deployError || rejected ? null : steps; + const title = realSteps + ? null + : (deployError ? 'deployment failed' : 'rejected'); return ( { this.renderStep() } @@ -158,7 +166,7 @@ export default class DeployContract extends Component { renderStep () { const { accounts, readOnly } = this.props; - const { address, deployError, step, deployState, txhash } = this.state; + const { address, deployError, step, deployState, txhash, rejected } = this.state; if (deployError) { return ( @@ -166,6 +174,15 @@ export default class DeployContract extends Component { ); } + if (rejected) { + return ( + + ); + } + switch (step) { case 0: return ( @@ -273,6 +290,11 @@ export default class DeployContract extends Component { }); }) .catch((error) => { + if (error.code === ERROR_CODES.REQUEST_REJECTED) { + this.setState({ rejected: true }); + return false; + } + console.error('error deploying contract', error); this.setState({ deployError: error }); store.dispatch({ type: 'newError', error }); diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index b45cf6875..a57c18a1d 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -23,6 +23,8 @@ import { validateAddress, validateUint } from '../../util/validation'; import DetailsStep from './DetailsStep'; +import { ERROR_CODES } from '../../api/transport/error'; + export default class ExecuteContract extends Component { static contextTypes = { api: PropTypes.object.isRequired, @@ -49,7 +51,8 @@ export default class ExecuteContract extends Component { step: 0, sending: false, busyState: null, - txhash: null + txhash: null, + rejected: false } componentDidMount () { @@ -80,6 +83,7 @@ export default class ExecuteContract extends Component { const { onClose, fromAddress } = this.props; const { sending, step, fromAddressError, valuesError } = this.state; const hasError = fromAddressError || valuesError.find((error) => error); + const cancelBtn = (