From 3cc007b4d6e344e03974a4a4bc5bc843d1669794 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 26 Feb 2017 18:29:35 +0100 Subject: [PATCH 01/93] add and remove column families dynamically --- util/src/kvdb.rs | 115 +++++++++++++++++++++++++++++++------- util/src/migration/mod.rs | 54 ++++++++++++------ 2 files changed, 132 insertions(+), 37 deletions(-) diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index 1714ce22f..add9528cc 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -410,6 +410,29 @@ struct DBAndColumns { cfs: Vec, } +// get column family configuration from database config. +fn col_config(col: u32, config: &DatabaseConfig) -> Options { + // default cache size for columns not specified. + const DEFAULT_CACHE: usize = 2; + + let mut opts = Options::new(); + opts.set_compaction_style(DBCompactionStyle::DBUniversalCompaction); + opts.set_target_file_size_base(config.compaction.initial_file_size); + opts.set_target_file_size_multiplier(config.compaction.file_size_multiplier); + + let col_opt = config.columns.map(|_| col); + + { + let cache_size = config.cache_sizes.get(&col_opt).cloned().unwrap_or(DEFAULT_CACHE); + let mut block_opts = BlockBasedOptions::new(); + // all goes to read cache. + block_opts.set_cache(Cache::new(cache_size * 1024 * 1024)); + opts.set_block_based_table_factory(&block_opts); + } + + opts +} + /// Key-Value database. pub struct Database { db: RwLock>, @@ -434,9 +457,6 @@ impl Database { /// Open database file. Creates if it does not exist. pub fn open(config: &DatabaseConfig, path: &str) -> Result { - // default cache size for columns not specified. - const DEFAULT_CACHE: usize = 2; - let mut opts = Options::new(); if let Some(rate_limit) = config.compaction.write_rate_limit { opts.set_parsed_options(&format!("rate_limiter_bytes_per_sec={}", rate_limit))?; @@ -460,22 +480,7 @@ impl Database { let cfnames: Vec<&str> = cfnames.iter().map(|n| n as &str).collect(); for col in 0 .. config.columns.unwrap_or(0) { - let mut opts = Options::new(); - opts.set_compaction_style(DBCompactionStyle::DBUniversalCompaction); - opts.set_target_file_size_base(config.compaction.initial_file_size); - opts.set_target_file_size_multiplier(config.compaction.file_size_multiplier); - - let col_opt = config.columns.map(|_| col); - - { - let cache_size = config.cache_sizes.get(&col_opt).cloned().unwrap_or(DEFAULT_CACHE); - let mut block_opts = BlockBasedOptions::new(); - // all goes to read cache. - block_opts.set_cache(Cache::new(cache_size * 1024 * 1024)); - opts.set_block_based_table_factory(&block_opts); - } - - cf_options.push(opts); + cf_options.push(col_config(col, &config)); } let mut write_opts = WriteOptions::new(); @@ -768,6 +773,42 @@ impl Database { *self.flushing.write() = mem::replace(&mut *db.flushing.write(), Vec::new()); Ok(()) } + + /// The number of non-default column families. + pub fn num_columns(&self) -> u32 { + self.db.read().as_ref() + .and_then(|db| if db.cfs.is_empty() { None } else { Some(db.cfs.len()) } ) + .map(|n| n as u32) + .unwrap_or(0) + } + + /// Drop a column family. + pub fn drop_column(&self) -> Result<(), String> { + match *self.db.write() { + Some(DBAndColumns { ref mut db, ref mut cfs }) => { + if let Some(col) = cfs.pop() { + let name = format!("col{}", cfs.len()); + drop(col); + db.drop_cf(&name)?; + } + Ok(()) + }, + None => Ok(()), + } + } + + /// Add a column family. + pub fn add_column(&self) -> Result<(), String> { + match *self.db.write() { + Some(DBAndColumns { ref mut db, ref mut cfs }) => { + let col = cfs.len() as u32; + let name = format!("col{}", col); + cfs.push(db.create_cf(&name, &col_config(col, &self.config))?); + Ok(()) + }, + None => Ok(()), + } + } } // duplicate declaration of methods here to avoid trait import in certain existing cases @@ -886,4 +927,40 @@ mod tests { let expected_output = Some(PathBuf::from("/sys/block/sda/queue/rotational")); assert_eq!(rotational_from_df_output(example_df), expected_output); } + + #[test] + fn dynamic_add_drop_columns() { + let config = DatabaseConfig::default(); + let config_5 = DatabaseConfig::with_columns(Some(5)); + + let path = RandomTempPath::create_dir(); + + // open empty, add 5. + { + let db = Database::open(&config, path.as_path().to_str().unwrap()).unwrap(); + assert_eq!(db.num_columns(), 0); + + for i in 0..5 { + db.add_column().unwrap(); + assert_eq!(db.num_columns(), i + 1); + } + } + + // open 5, remove all. + { + let db = Database::open(&config_5, path.as_path().to_str().unwrap()).unwrap(); + assert_eq!(db.num_columns(), 5); + + for i in (0..5).rev() { + db.drop_column().unwrap(); + assert_eq!(db.num_columns(), i); + } + } + + // reopen as 0. + { + let db = Database::open(&config, path.as_path().to_str().unwrap()).unwrap(); + assert_eq!(db.num_columns(), 0); + } + } } diff --git a/util/src/migration/mod.rs b/util/src/migration/mod.rs index 50464444f..3af86fc5b 100644 --- a/util/src/migration/mod.rs +++ b/util/src/migration/mod.rs @@ -127,6 +127,9 @@ pub trait Migration: 'static { fn pre_columns(&self) -> Option { self.columns() } /// Number of columns in database after the migration. fn columns(&self) -> Option; + /// Whether this migration alters any existing columns. + /// if not, then column families will simply be added and `migrate` will never be called. + fn alters_existing(&self) -> bool { true } /// Version of the database after the migration. fn version(&self) -> u32; /// Migrate a source to a destination. @@ -149,6 +152,8 @@ impl Migration for T { fn version(&self) -> u32 { SimpleMigration::version(self) } + fn alters_existing(&self) -> bool { true } + fn migrate(&mut self, source: Arc, config: &Config, dest: &mut Database, col: Option) -> Result<(), Error> { let mut batch = Batch::new(config, col); @@ -256,28 +261,41 @@ impl Manager { let current_columns = db_config.columns; db_config.columns = migration.columns(); - // open the target temporary database. - temp_path = temp_idx.path(&db_root); - let temp_path_str = temp_path.to_str().ok_or(Error::MigrationImpossible)?; - let mut new_db = Database::open(&db_config, temp_path_str).map_err(Error::Custom)?; + // slow migrations: alter existing data. + if migration.alters_existing() { + // open the target temporary database. + temp_path = temp_idx.path(&db_root); + let temp_path_str = temp_path.to_str().ok_or(Error::MigrationImpossible)?; + let mut new_db = Database::open(&db_config, temp_path_str).map_err(Error::Custom)?; - // perform the migration from cur_db to new_db. - match current_columns { - // migrate only default column - None => migration.migrate(cur_db.clone(), &config, &mut new_db, None)?, - Some(v) => { - // Migrate all columns in previous DB - for col in 0..v { - migration.migrate(cur_db.clone(), &config, &mut new_db, Some(col))? + match current_columns { + // migrate only default column + None => migration.migrate(cur_db.clone(), &config, &mut new_db, None)?, + Some(v) => { + // Migrate all columns in previous DB + for col in 0..v { + migration.migrate(cur_db.clone(), &config, &mut new_db, Some(col))? + } } } - } - // next iteration, we will migrate from this db into the other temp. - cur_db = Arc::new(new_db); - temp_idx.swap(); + // next iteration, we will migrate from this db into the other temp. + cur_db = Arc::new(new_db); + temp_idx.swap(); - // remove the other temporary migration database. - let _ = fs::remove_dir_all(temp_idx.path(&db_root)); + // remove the other temporary migration database. + let _ = fs::remove_dir_all(temp_idx.path(&db_root)); + } else { + // migrations which simply add or remove column families. + // we can do this in-place. + let goal_columns = migration.columns().unwrap_or(0); + while cur_db.num_columns() < goal_columns { + cur_db.add_column().map_err(Error::Custom)?; + } + + while cur_db.num_columns() > goal_columns { + cur_db.drop_column().map_err(Error::Custom)?; + } + } } Ok(temp_path) } From c2c699abb9e4461f85c5639991744d3b7d30f5a3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 26 Feb 2017 18:41:40 +0100 Subject: [PATCH 02/93] change migration to v11 to be faster --- ethcore/src/migrations/mod.rs | 2 +- ethcore/src/migrations/v11.rs | 34 +++++++--------------------------- parity/migration.rs | 2 +- util/src/migration/mod.rs | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/ethcore/src/migrations/mod.rs b/ethcore/src/migrations/mod.rs index b9a00a15e..6cc4a13a8 100644 --- a/ethcore/src/migrations/mod.rs +++ b/ethcore/src/migrations/mod.rs @@ -28,4 +28,4 @@ mod v10; pub use self::v10::ToV10; mod v11; -pub use self::v11::ToV11; +pub use self::v11::TO_V11; diff --git a/ethcore/src/migrations/v11.rs b/ethcore/src/migrations/v11.rs index 8795cf364..e33de6170 100644 --- a/ethcore/src/migrations/v11.rs +++ b/ethcore/src/migrations/v11.rs @@ -14,33 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . - //! Adds a seventh column for node information. -use util::kvdb::Database; -use util::migration::{Batch, Config, Error, Migration, Progress}; -use std::sync::Arc; +use util::migration::ChangeColumns; -/// Copies over data for all existing columns. -#[derive(Default)] -pub struct ToV11(Progress); - - -impl Migration for ToV11 { - fn pre_columns(&self) -> Option { Some(6) } - fn columns(&self) -> Option { Some(7) } - - fn version(&self) -> u32 { 11 } - - fn migrate(&mut self, source: Arc, config: &Config, dest: &mut Database, col: Option) -> Result<(), Error> { - // just copy everything over. - let mut batch = Batch::new(config, col); - - for (key, value) in source.iter(col) { - self.0.tick(); - batch.insert(key.to_vec(), value.to_vec(), dest)? - } - - batch.commit(dest) - } -} +/// The migration from v10 to v11. +pub const TO_V11: ChangeColumns = ChangeColumns { + pre_columns: Some(6), + post_columns: Some(7), + version: 11, +}; diff --git a/parity/migration.rs b/parity/migration.rs index c2d5c0797..797e8d2d6 100644 --- a/parity/migration.rs +++ b/parity/migration.rs @@ -146,7 +146,7 @@ pub fn default_migration_settings(compaction_profile: &CompactionProfile) -> Mig fn consolidated_database_migrations(compaction_profile: &CompactionProfile) -> Result { let mut manager = MigrationManager::new(default_migration_settings(compaction_profile)); manager.add_migration(migrations::ToV10::new()).map_err(|_| Error::MigrationImpossible)?; - manager.add_migration(migrations::ToV11::default()).map_err(|_| Error::MigrationImpossible)?; + manager.add_migration(migrations::TO_V11).map_err(|_| Error::MigrationImpossible)?; Ok(manager) } diff --git a/util/src/migration/mod.rs b/util/src/migration/mod.rs index 3af86fc5b..3f0a7a806 100644 --- a/util/src/migration/mod.rs +++ b/util/src/migration/mod.rs @@ -167,6 +167,23 @@ impl Migration for T { } } +/// An even simpler migration which just changes the number of columns. +pub struct ChangeColumns { + pub pre_columns: Option, + pub post_columns: Option, + pub version: u32, +} + +impl Migration for ChangeColumns { + fn pre_columns(&self) -> Option { self.pre_columns } + fn columns(&self) -> Option { self.post_columns } + fn version(&self) -> u32 { self.version } + fn alters_existing(&self) -> bool { false } + fn migrate(&mut self, _: Arc, _: &Config, _: &mut Database, _: Option) -> Result<(), Error> { + Ok(()) + } +} + /// Get the path where all databases reside. fn database_path(path: &Path) -> PathBuf { let mut temp_path = path.to_owned(); From ed0a2567d869d731f426fd8f779c8567877e7983 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 26 Feb 2017 19:11:19 +0100 Subject: [PATCH 03/93] docs --- util/src/migration/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util/src/migration/mod.rs b/util/src/migration/mod.rs index 3f0a7a806..f4a00fee3 100644 --- a/util/src/migration/mod.rs +++ b/util/src/migration/mod.rs @@ -169,8 +169,11 @@ impl Migration for T { /// An even simpler migration which just changes the number of columns. pub struct ChangeColumns { + /// The amount of columns before this migration. pub pre_columns: Option, + /// The amount of columns after this migration. pub post_columns: Option, + /// The version after this migration. pub version: u32, } @@ -277,11 +280,11 @@ impl Manager { // Change number of columns in new db let current_columns = db_config.columns; db_config.columns = migration.columns(); + temp_path = temp_idx.path(&db_root); // slow migrations: alter existing data. if migration.alters_existing() { // open the target temporary database. - temp_path = temp_idx.path(&db_root); let temp_path_str = temp_path.to_str().ok_or(Error::MigrationImpossible)?; let mut new_db = Database::open(&db_config, temp_path_str).map_err(Error::Custom)?; From ac82a838b84b41556feefd1be1a6736f59765d10 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 26 Feb 2017 19:22:51 +0100 Subject: [PATCH 04/93] test case and handle in-place migration correctly --- parity/migration.rs | 24 ++++++++++++++---------- util/src/migration/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/parity/migration.rs b/parity/migration.rs index 797e8d2d6..0d99bf250 100644 --- a/parity/migration.rs +++ b/parity/migration.rs @@ -201,19 +201,23 @@ fn migrate_database(version: u32, db_path: PathBuf, mut migrations: MigrationMan // migrate old database to the new one let temp_path = migrations.execute(&db_path, version)?; - // create backup - fs::rename(&db_path, &backup_path)?; + // completely in-place migration leads to the paths being equal. + // in that case, no need to shuffle directories. + if temp_path != db_path { + // create backup + fs::rename(&db_path, &backup_path)?; - // replace the old database with the new one - if let Err(err) = fs::rename(&temp_path, &db_path) { - // if something went wrong, bring back backup - fs::rename(&backup_path, &db_path)?; - return Err(err.into()); + // replace the old database with the new one + if let Err(err) = fs::rename(&temp_path, &db_path) { + // if something went wrong, bring back backup + fs::rename(&backup_path, &db_path)?; + return Err(err.into()); + } + + // remove backup + fs::remove_dir_all(&backup_path)?; } - // remove backup - fs::remove_dir_all(&backup_path)?; - Ok(()) } diff --git a/util/src/migration/tests.rs b/util/src/migration/tests.rs index 852b96a8d..ad80ddbc3 100644 --- a/util/src/migration/tests.rs +++ b/util/src/migration/tests.rs @@ -226,3 +226,24 @@ fn pre_columns() { // short of the one before it. manager.execute(&db_path, 0).unwrap(); } + +#[test] +fn change_columns() { + use kvdb::DatabaseConfig; + + let mut manager = Manager::new(Config::default()); + manager.add_migration(::migration::ChangeColumns { + pre_columns: None, + post_columns: Some(4), + version: 1, + }).unwrap(); + + let dir = RandomTempPath::create_dir(); + let db_path = db_path(dir.as_path()); + + let new_path = manager.execute(&db_path, 0).unwrap(); + + let config = DatabaseConfig::with_columns(Some(4)); + let db = Database::open(&config, new_path.to_str().unwrap()).unwrap(); + assert_eq!(db.num_columns(), 4); +} From 868624c6a9f05d43bc89174a0d0804da3f62ddcc Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sun, 26 Feb 2017 19:30:54 +0100 Subject: [PATCH 05/93] return correct path for in-place migration --- util/src/migration/mod.rs | 5 +++-- util/src/migration/tests.rs | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/util/src/migration/mod.rs b/util/src/migration/mod.rs index f4a00fee3..6ec465517 100644 --- a/util/src/migration/mod.rs +++ b/util/src/migration/mod.rs @@ -269,7 +269,7 @@ impl Manager { let db_root = database_path(old_path); let mut temp_idx = TempIndex::One; - let mut temp_path = temp_idx.path(&db_root); + let mut temp_path = old_path.to_path_buf(); // start with the old db. let old_path_str = old_path.to_str().ok_or(Error::MigrationImpossible)?; @@ -280,10 +280,11 @@ impl Manager { // Change number of columns in new db let current_columns = db_config.columns; db_config.columns = migration.columns(); - temp_path = temp_idx.path(&db_root); // slow migrations: alter existing data. if migration.alters_existing() { + temp_path = temp_idx.path(&db_root); + // open the target temporary database. let temp_path_str = temp_path.to_str().ok_or(Error::MigrationImpossible)?; let mut new_db = Database::open(&db_config, temp_path_str).map_err(Error::Custom)?; diff --git a/util/src/migration/tests.rs b/util/src/migration/tests.rs index ad80ddbc3..31226ec49 100644 --- a/util/src/migration/tests.rs +++ b/util/src/migration/tests.rs @@ -243,6 +243,8 @@ fn change_columns() { let new_path = manager.execute(&db_path, 0).unwrap(); + assert_eq!(db_path, new_path, "Changing columns is an in-place migration."); + let config = DatabaseConfig::with_columns(Some(4)); let db = Database::open(&config, new_path.to_str().unwrap()).unwrap(); assert_eq!(db.num_columns(), 4); From 2f15c75fa2de3e6ef04ec4a7a0746c761230fbb6 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" Date: Mon, 27 Feb 2017 01:13:27 +0400 Subject: [PATCH 06/93] update gitlab-ci fix path to Dockerfile [ci skip] --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d193039b5..84d8638fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -499,7 +499,6 @@ docker-build: before_script: - docker info script: - - cd docker/hub - if [ "$CI_BUILD_REF_NAME" == "nightly" ]; then DOCKER_TAG="latest"; else DOCKER_TAG=$CI_BUILD_REF_NAME; fi - docker login -u $Docker_Hub_User -p $Docker_Hub_Pass - sh scripts/docker-build.sh $DOCKER_TAG From b13a446d8244cdc211fcd4842b885b53c72c918e Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" Date: Mon, 27 Feb 2017 01:14:23 +0400 Subject: [PATCH 07/93] update docker-build fix path --- scripts/docker-build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index bc6185d15..9c874eac6 100644 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -1,3 +1,4 @@ #!/bin/bash +cd docker/hub docker build --no-cache=true --tag ethcore/parity:$1 . docker push ethcore/parity:$1 From da3c13f0a2a8138242c764d1d0e262509ad0f32c Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 27 Feb 2017 18:57:22 +0100 Subject: [PATCH 08/93] split adding and dropping columns tests --- util/src/kvdb.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index add9528cc..043b3d983 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -929,7 +929,7 @@ mod tests { } #[test] - fn dynamic_add_drop_columns() { + fn add_columns() { let config = DatabaseConfig::default(); let config_5 = DatabaseConfig::with_columns(Some(5)); @@ -946,6 +946,20 @@ mod tests { } } + // reopen as 5. + { + let db = Database::open(&config_5, path.as_path().to_str().unwrap()).unwrap(); + assert_eq!(db.num_columns(), 5); + } + } + + #[test] + fn drop_columns() { + let config = DatabaseConfig::default(); + let config_5 = DatabaseConfig::with_columns(Some(5)); + + let path = RandomTempPath::create_dir(); + // open 5, remove all. { let db = Database::open(&config_5, path.as_path().to_str().unwrap()).unwrap(); From b487f001734ac588feac0b0138d57fae1531da5b Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Mon, 27 Feb 2017 19:02:16 +0100 Subject: [PATCH 09/93] address rightward drift --- parity/migration.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/parity/migration.rs b/parity/migration.rs index 0d99bf250..445724325 100644 --- a/parity/migration.rs +++ b/parity/migration.rs @@ -203,22 +203,20 @@ fn migrate_database(version: u32, db_path: PathBuf, mut migrations: MigrationMan // completely in-place migration leads to the paths being equal. // in that case, no need to shuffle directories. - if temp_path != db_path { - // create backup - fs::rename(&db_path, &backup_path)?; + if temp_path == db_path { return Ok(()) } - // replace the old database with the new one - if let Err(err) = fs::rename(&temp_path, &db_path) { - // if something went wrong, bring back backup - fs::rename(&backup_path, &db_path)?; - return Err(err.into()); - } + // create backup + fs::rename(&db_path, &backup_path)?; - // remove backup - fs::remove_dir_all(&backup_path)?; + // replace the old database with the new one + if let Err(err) = fs::rename(&temp_path, &db_path) { + // if something went wrong, bring back backup + fs::rename(&backup_path, &db_path)?; + return Err(err.into()); } - Ok(()) + // remove backup + fs::remove_dir_all(&backup_path).map_err(Into::into) } fn exists(path: &Path) -> bool { From 5e480e9fc017e3e69cd28bfc968944ada933e010 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 27 Feb 2017 22:28:21 +0100 Subject: [PATCH 10/93] Less agressive grayscale/opacity (#4688) --- js/src/ui/SelectionList/selectionList.css | 17 ++++++++++++----- js/src/ui/SelectionList/selectionList.js | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/js/src/ui/SelectionList/selectionList.css b/js/src/ui/SelectionList/selectionList.css index eee61dbb5..f12b10af2 100644 --- a/js/src/ui/SelectionList/selectionList.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -16,6 +16,7 @@ */ .item { + border: 2px solid transparent; display: flex; flex: 1; height: 100%; @@ -23,7 +24,9 @@ width: 100%; &:hover { - box-shadow: inset 0 0 0 2px rgb(255, 255, 255); + border-color: transparent; + filter: none; + opacity: 1; } .content { @@ -31,7 +34,7 @@ width: 100%; &:hover { - background: rgba(255, 255, 255, 0.25); + background-color: rgba(255, 255, 255, 0.5); } } @@ -43,13 +46,17 @@ } .selected { - box-shadow: inset 0 0 0 2px rgb(255, 255, 255); + border-color: rgba(255, 255, 255, 0.25); filter: none; + + &.default { + border-color: rgba(255, 255, 255, 0.75); + } } .unselected { - filter: grayscale(100%); - opacity: 0.5; + filter: grayscale(10%); + opacity: 0.75; } .iconDisabled { diff --git a/js/src/ui/SelectionList/selectionList.js b/js/src/ui/SelectionList/selectionList.js index 2293cf9c3..1e38d39b0 100644 --- a/js/src/ui/SelectionList/selectionList.js +++ b/js/src/ui/SelectionList/selectionList.js @@ -71,6 +71,10 @@ export default class SelectionList extends Component { ? [styles.item, styles.selected] : [styles.item, styles.unselected]; + if (item.default) { + classes.push(styles.default); + } + return (
Date: Mon, 27 Feb 2017 21:40:33 +0000 Subject: [PATCH 11/93] [ci skip] js-precompiled 20170227-213540 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46b1ed27c..a934bf857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#aaee793907e4ff61082d83ff44733363dfff6eae" +source = "git+https://github.com/ethcore/js-precompiled.git#c0673fa6e5d1cd96136de374ab29c1f119be0645" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 0b5cdbaaf..5586b988a 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.105", + "version": "0.3.106", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From 1b8a47fa4a66cc0bf8ec18a3c695fa15e5fa7bd7 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 28 Feb 2017 14:15:48 +0100 Subject: [PATCH 12/93] Display ... for address summary overflows (#4691) --- js/src/views/Accounts/Summary/summary.js | 18 +++++++----------- js/src/views/Accounts/accounts.css | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 6850e1035..c6a9f73ff 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -22,7 +22,7 @@ import { isEqual } from 'lodash'; import ReactTooltip from 'react-tooltip'; import { FormattedMessage } from 'react-intl'; -import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui'; +import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags } from '~/ui'; import Certifications from '~/ui/Certifications'; import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; @@ -101,15 +101,6 @@ class Summary extends Component { const { address } = account; - const addressComponent = ( - - ); - return ( + +
{ address }
+
+ } className={ noLink ? styles.main diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index 9a3d747ce..a8bd97a72 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -23,6 +23,19 @@ .account { position: relative; + .addressline { + display: flex; + white-space: nowrap; + + .address { + display: inline-block; + flex: 1; + margin-left: 0.5em; + overflow: hidden; + text-overflow: ellipsis; + } + } + .blockDescription { color: rgba(255, 255, 255, 0.25); margin-top: 1.5em; @@ -95,10 +108,12 @@ .heading { display: flex; flex-direction: row; + overflow: hidden; .main, .mainLink { flex: 1; + overflow: hidden; } .mainLink h3 { From ab98ec3bf7cbe04f11a17d30ed07e5323b45d5df Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 28 Feb 2017 14:20:43 +0100 Subject: [PATCH 13/93] Stop onClick propagation after click (#4700) --- js/src/ui/CopyToClipboard/copyToClipboard.js | 45 ++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/js/src/ui/CopyToClipboard/copyToClipboard.js b/js/src/ui/CopyToClipboard/copyToClipboard.js index 0afd967fd..016730fce 100644 --- a/js/src/ui/CopyToClipboard/copyToClipboard.js +++ b/js/src/ui/CopyToClipboard/copyToClipboard.js @@ -16,6 +16,7 @@ import { IconButton } from 'material-ui'; import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import Clipboard from 'react-copy-to-clipboard'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -64,14 +65,33 @@ class CopyToClipboard extends Component { const { copied } = this.state; return ( - -
+ +
- +
@@ -82,9 +102,13 @@ class CopyToClipboard extends Component { const { data, onCopy, cooldown, showSnackbar } = this.props; const message = (
- copied - { data } - to clipboard + { data } + } } + />
); @@ -98,6 +122,11 @@ class CopyToClipboard extends Component { showSnackbar(message, cooldown); onCopy(); } + + onClick = (event) => { + event.stopPropagation(); + event.preventDefault(); + } } function mapDispatchToProps (dispatch) { From 88449671a1b76e09a577247f727037d7f942f341 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 28 Feb 2017 14:21:19 +0100 Subject: [PATCH 14/93] Rename https://mkr.market -> https://oasisdex.com (#4701) --- js/src/views/Web/store.js | 2 +- js/src/views/Web/web.spec.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/js/src/views/Web/store.js b/js/src/views/Web/store.js index e62effb89..4eed3eaf3 100644 --- a/js/src/views/Web/store.js +++ b/js/src/views/Web/store.js @@ -20,7 +20,7 @@ import { parse as parseUrl } from 'url'; import { encodePath, encodeUrl } from '~/util/dapplink'; -const DEFAULT_URL = 'https://mkr.market'; +const DEFAULT_URL = 'https://oasisdex.com'; const LS_LAST_ADDRESS = '_parity::webLastAddress'; const hasProtocol = /^https?:\/\//; diff --git a/js/src/views/Web/web.spec.js b/js/src/views/Web/web.spec.js index 94a125afa..950806d0b 100644 --- a/js/src/views/Web/web.spec.js +++ b/js/src/views/Web/web.spec.js @@ -17,10 +17,9 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { DEFAULT_URL } from './store'; import Web from './'; -const TEST_URL = 'https://mkr.market'; - let api; let component; @@ -30,7 +29,7 @@ function createApi () { return api; } -function render (url = TEST_URL) { +function render (url = DEFAULT_URL) { component = shallow( , { From 190ed76e30a5fa7d357e8bfb78adfa687a673635 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 28 Feb 2017 14:24:12 +0100 Subject: [PATCH 15/93] Consistent file uploads (#4699) * FileSelect component * Use FileSelect component in Actionbar * Convert CreateAccount/Import to FileSelect --- .../CreateAccount/NewImport/newImport.js | 89 ++++++------- js/src/modals/CreateAccount/createAccount.css | 16 +-- js/src/modals/CreateAccount/store.js | 3 + js/src/modals/CreateAccount/store.spec.js | 11 ++ js/src/ui/Actionbar/Import/import.css | 24 ---- js/src/ui/Actionbar/Import/import.js | 64 +++------- js/src/ui/Form/FileSelect/fileSelect.css | 52 ++++++++ js/src/ui/Form/FileSelect/fileSelect.js | 76 +++++++++++ js/src/ui/Form/FileSelect/fileSelect.spec.js | 118 ++++++++++++++++++ js/src/ui/Form/FileSelect/index.js | 17 +++ js/src/ui/Form/index.js | 1 + js/src/ui/index.js | 2 +- 12 files changed, 340 insertions(+), 133 deletions(-) create mode 100644 js/src/ui/Form/FileSelect/fileSelect.css create mode 100644 js/src/ui/Form/FileSelect/fileSelect.js create mode 100644 js/src/ui/Form/FileSelect/fileSelect.spec.js create mode 100644 js/src/ui/Form/FileSelect/index.js diff --git a/js/src/modals/CreateAccount/NewImport/newImport.js b/js/src/modals/CreateAccount/NewImport/newImport.js index 7f4a6bd1a..121f0be57 100644 --- a/js/src/modals/CreateAccount/NewImport/newImport.js +++ b/js/src/modals/CreateAccount/NewImport/newImport.js @@ -14,19 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { FloatingActionButton } from 'material-ui'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; -import ReactDOM from 'react-dom'; import { FormattedMessage } from 'react-intl'; -import { Form, Input } from '~/ui'; -import { AttachFileIcon } from '~/ui/Icons'; +import { Form, FileSelect, Input } from '~/ui'; import styles from '../createAccount.css'; -const STYLE_HIDDEN = { display: 'none' }; - @observer export default class NewImport extends Component { static propTypes = { @@ -34,7 +29,7 @@ export default class NewImport extends Component { } render () { - const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store; + const { name, nameError, password, passwordHint } = this.props.store; return (
@@ -93,58 +88,48 @@ export default class NewImport extends Component { />
-
- - } - label={ - - } - value={ walletFile } - /> -
- - - - -
-
+ { this.renderFileSelector() } ); } - onFileChange = (event) => { - const { store } = this.props; + renderFileSelector () { + const { walletFile, walletFileError } = this.props.store; - if (event.target.files.length) { - const reader = new FileReader(); - - reader.onload = (event) => store.setWalletJson(event.target.result); - reader.readAsText(event.target.files[0]); - } - - store.setWalletFile(event.target.value); + return walletFile + ? ( + + } + label={ + + } + value={ walletFile } + /> + ) + : ( + + ); } - openFileDialog = () => { - ReactDOM.findDOMNode(this.refs.fileUpload).click(); + onFileSelect = (fileName, fileContent) => { + const { store } = this.props; + + store.setWalletFile(fileName); + store.setWalletJson(fileContent); } onEditName = (event, name) => { diff --git a/js/src/modals/CreateAccount/createAccount.css b/js/src/modals/CreateAccount/createAccount.css index eb9ff1e5c..46e327e90 100644 --- a/js/src/modals/CreateAccount/createAccount.css +++ b/js/src/modals/CreateAccount/createAccount.css @@ -101,17 +101,6 @@ width: 10% !important; } -.upload { - text-align: right; - float: right; - margin-left: -100%; - margin-top: 28px; -} - -.upload>div { - margin-right: 0.5em; -} - .checkbox { margin-top: 2em; } @@ -131,6 +120,11 @@ } } +.fileImport { + height: 9em; + margin-top: 1em; +} + .summary { line-height: 1.618em; padding: 0 4em 1.5em 4em; diff --git a/js/src/modals/CreateAccount/store.js b/js/src/modals/CreateAccount/store.js index 1875ebe76..7371e8df3 100644 --- a/js/src/modals/CreateAccount/store.js +++ b/js/src/modals/CreateAccount/store.js @@ -93,8 +93,11 @@ export default class Store { this.phrase = ''; this.name = ''; this.nameError = null; + this.rawKey = ''; this.rawKeyError = null; + this.walletFile = ''; this.walletFileError = null; + this.walletJson = ''; }); } diff --git a/js/src/modals/CreateAccount/store.spec.js b/js/src/modals/CreateAccount/store.spec.js index 9c74365c9..67303fa21 100644 --- a/js/src/modals/CreateAccount/store.spec.js +++ b/js/src/modals/CreateAccount/store.spec.js @@ -64,13 +64,24 @@ describe('modals/CreateAccount/Store', () => { describe('@action', () => { describe('clearErrors', () => { + beforeEach(() => { + store.setName(''); + store.setPassword('123'); + store.setRawKey('test'); + store.setWalletFile('test'); + store.setWalletJson('test'); + }); + it('clears all errors', () => { store.clearErrors(); expect(store.nameError).to.be.null; expect(store.passwordRepeatError).to.be.null; + expect(store.rawKey).to.equal(''); expect(store.rawKeyError).to.be.null; + expect(store.walletFile).to.equal(''); expect(store.walletFileError).to.be.null; + expect(store.walletJson).to.equal(''); }); }); diff --git a/js/src/ui/Actionbar/Import/import.css b/js/src/ui/Actionbar/Import/import.css index da71e5f15..53a3e049a 100644 --- a/js/src/ui/Actionbar/Import/import.css +++ b/js/src/ui/Actionbar/Import/import.css @@ -15,30 +15,6 @@ /* along with Parity. If not, see . */ -.importZone { - width: 100%; - height: 200px; - border-width: 2px; - border-color: #666; - border-style: dashed; - border-radius: 10px; - - background-color: rgba(50, 50, 50, 0.2); - - display: flex; - align-items: center; - justify-content: center; - font-size: 1.2em; - - transition: all 0.5s ease; - - &:hover { - cursor: pointer; - border-radius: 0; - background-color: rgba(50, 50, 50, 0.5); - } -} - .desc { margin-top: 0; color: #ccc; diff --git a/js/src/ui/Actionbar/Import/import.js b/js/src/ui/Actionbar/Import/import.js index 91bdefe80..ada951cd9 100644 --- a/js/src/ui/Actionbar/Import/import.js +++ b/js/src/ui/Actionbar/Import/import.js @@ -15,12 +15,12 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import Dropzone from 'react-dropzone'; import { FormattedMessage } from 'react-intl'; import { nodeOrStringProptype } from '~/util/proptypes'; import Button from '../../Button'; +import FileSelect from '../../Form/FileSelect'; import { CancelIcon, DoneIcon, FileUploadIcon } from '../../Icons'; import Portal from '../../Portal'; @@ -184,25 +184,8 @@ export default class ActionbarImport extends Component { return this.renderValidation(); } - return this.renderFileSelect(); - } - - renderFileSelect () { return ( -
- -
- -
-
-
+ ); } @@ -224,39 +207,30 @@ export default class ActionbarImport extends Component { ); } - onDrop = (files) => { + onFileSelect = (file, content) => { const { renderValidation } = this.props; - const file = files[0]; - const reader = new FileReader(); + if (typeof renderValidation !== 'function') { + this.props.onConfirm(content); + return this.onCloseModal(); + } - reader.onload = (e) => { - const content = e.target.result; + const validationBody = renderValidation(content); - if (typeof renderValidation !== 'function') { - this.props.onConfirm(content); - return this.onCloseModal(); - } - - const validationBody = renderValidation(content); - - if (validationBody && validationBody.error) { - return this.setState({ - step: 1, - error: true, - errorText: validationBody.error - }); - } - - this.setState({ + if (validationBody && validationBody.error) { + return this.setState({ step: 1, - validate: true, - validationBody, - content + error: true, + errorText: validationBody.error }); - }; + } - reader.readAsText(file); + this.setState({ + step: 1, + validate: true, + validationBody, + content + }); } onConfirm = () => { diff --git a/js/src/ui/Form/FileSelect/fileSelect.css b/js/src/ui/Form/FileSelect/fileSelect.css new file mode 100644 index 000000000..1ac5981c4 --- /dev/null +++ b/js/src/ui/Form/FileSelect/fileSelect.css @@ -0,0 +1,52 @@ +/* Copyright 2015-2017 Parity Technologies (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 . +*/ + +$backgroundNormal: rgba(0, 0, 0, 0.2); +$backgroundNormalHover: rgba(0, 0, 0, 0.5); +$backgroundError: rgba(255, 0, 0, 0.1); +$backgroundErrorHover: rgba(255, 0, 0, 0.2); + +.dropzone { + align-items: center; + background: $backgroundNormal; + border: 2px dashed #666; + border-radius: 0.5em; + display: flex; + justify-content: center; + font-size: 1.2em; + height: 12em; + transition: all 0.5s ease; + width: 100%; + + &:hover { + background: $backgroundNormalHover; + border-radius: 0; + cursor: pointer; + } + + &.error { + background: $backgroundError; + + &:hover { + background: $backgroundErrorHover; + } + } + + .label { + color: #aaa; + } +} diff --git a/js/src/ui/Form/FileSelect/fileSelect.js b/js/src/ui/Form/FileSelect/fileSelect.js new file mode 100644 index 000000000..18b2c8d8e --- /dev/null +++ b/js/src/ui/Form/FileSelect/fileSelect.js @@ -0,0 +1,76 @@ +// Copyright 2015-2017 Parity Technologies (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 Dropzone from 'react-dropzone'; +import { FormattedMessage } from 'react-intl'; + +import { nodeOrStringProptype } from '~/util/proptypes'; + +import styles from './fileSelect.css'; + +export default class FileSelect extends Component { + static propTypes = { + className: PropTypes.string, + error: nodeOrStringProptype(), + label: nodeOrStringProptype(), + onSelect: PropTypes.func.isRequired + } + + static defaultProps = { + label: ( + + ) + } + + render () { + const { className, error, label } = this.props; + + return ( + +
+ { error || label } +
+
+ ); + } + + onDrop = (files) => { + const { onSelect } = this.props; + const reader = new FileReader(); + const file = files[0]; + + reader.onload = (event) => { + onSelect(file.name, event.target.result); + }; + + reader.readAsText(file); + } +} diff --git a/js/src/ui/Form/FileSelect/fileSelect.spec.js b/js/src/ui/Form/FileSelect/fileSelect.spec.js new file mode 100644 index 000000000..f931fc655 --- /dev/null +++ b/js/src/ui/Form/FileSelect/fileSelect.spec.js @@ -0,0 +1,118 @@ +// Copyright 2015-2017 Parity Technologies (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 { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import FileSelect from './'; + +const FILE = { + content: 'some test content', + name: 'someName' +}; + +let component; +let globalFileReader; +let instance; +let onSelect; +let processedFile; + +function stubReader () { + globalFileReader = global.FileReader; + + global.FileReader = class { + readAsText (file) { + processedFile = file; + + this.onload({ + target: { + result: file.content + } + }); + } + }; +} + +function restoreReader () { + global.FileReader = globalFileReader; +} + +function render (props = {}) { + onSelect = sinon.stub(); + component = shallow( + + ); + instance = component.instance(); + + return component; +} + +describe('ui/Form/FileSelect', () => { + beforeEach(() => { + stubReader(); + render(); + }); + + afterEach(() => { + restoreReader(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('DropZone', () => { + let label; + let zone; + + beforeEach(() => { + label = component.find('FormattedMessage'); + zone = component.find('Dropzone'); + }); + + it('renders the label', () => { + expect(label.props().id).to.equal('ui.fileSelect.defaultLabel'); + }); + + it('attaches the onDrop event', () => { + expect(zone.props().onDrop).to.equal(instance.onDrop); + }); + + it('does not allow multiples', () => { + expect(zone.props().multiple).to.be.false; + }); + }); + + describe('event methods', () => { + describe('onDrop', () => { + beforeEach(() => { + instance.onDrop([ FILE ]); + }); + + it('reads the file as dropped', () => { + expect(processedFile).to.deep.equal(FILE); + }); + + it('calls prop onSelect with file & content', () => { + expect(onSelect).to.have.been.calledWith(FILE.name, FILE.content); + }); + }); + }); +}); diff --git a/js/src/ui/Form/FileSelect/index.js b/js/src/ui/Form/FileSelect/index.js new file mode 100644 index 000000000..ff9614ace --- /dev/null +++ b/js/src/ui/Form/FileSelect/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (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 './fileSelect'; diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index 6a003c472..bb5516c89 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -16,6 +16,7 @@ export AddressSelect from './AddressSelect'; export DappUrlInput from './DappUrlInput'; +export FileSelect from './FileSelect'; export FormWrap from './FormWrap'; export Input from './Input'; export InputAddress from './InputAddress'; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 992bb2b05..ae1ce8451 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -30,7 +30,7 @@ export DappCard from './DappCard'; export DappIcon from './DappIcon'; export Errors from './Errors'; export Features, { FEATURES, FeaturesStore } from './Features'; -export Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; +export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; export GasPriceEditor from './GasPriceEditor'; export GasPriceSelector from './GasPriceSelector'; export Icons from './Icons'; From ea877cb91e10497adf50f33dfac3f0596bc015cb Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Tue, 28 Feb 2017 13:35:38 +0000 Subject: [PATCH 16/93] [ci skip] js-precompiled 20170228-132909 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a934bf857..de9d6e922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#c0673fa6e5d1cd96136de374ab29c1f119be0645" +source = "git+https://github.com/ethcore/js-precompiled.git#d3afc8594280284e0cc75b8db123ec543085ddc0" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 5586b988a..8dcbfe263 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.106", + "version": "0.3.107", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From 4534590f3981f9b7546493366dcb55cba723fd05 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 28 Feb 2017 16:31:22 +0100 Subject: [PATCH 17/93] Display badges on summary view (#4689) --- js/src/ui/Balance/balance.css | 1 + js/src/ui/Balance/balance.js | 2 +- js/src/ui/Certifications/certifications.css | 51 +++++++++++++-------- js/src/ui/Certifications/certifications.js | 48 +++++++++++++++---- js/src/views/Accounts/Summary/summary.js | 14 ++++-- js/src/views/Accounts/accounts.css | 19 +++++++- 6 files changed, 101 insertions(+), 34 deletions(-) diff --git a/js/src/ui/Balance/balance.css b/js/src/ui/Balance/balance.css index ed0f79e7b..81e0fa798 100644 --- a/js/src/ui/Balance/balance.css +++ b/js/src/ui/Balance/balance.css @@ -28,6 +28,7 @@ } .empty { + min-height: 24px; line-height: 24px; opacity: 0.25; overflow: hidden; diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js index 04fed6942..18652fa85 100644 --- a/js/src/ui/Balance/balance.js +++ b/js/src/ui/Balance/balance.js @@ -96,7 +96,7 @@ export default class Balance extends Component {
); diff --git a/js/src/ui/Certifications/certifications.css b/js/src/ui/Certifications/certifications.css index 0b1930c6a..8e16cf2d5 100644 --- a/js/src/ui/Certifications/certifications.css +++ b/js/src/ui/Certifications/certifications.css @@ -19,29 +19,40 @@ margin-top: 1em; } -.certification { +.certification, +.certificationIcon { + border-radius: 1em; display: inline-block; - position: relative; - margin-top: 1em; - margin-right: .5em; - padding: .3em .6em .2em 2.6em; - border-radius: 1em; line-height: 1em; - text-transform: uppercase; + position: relative; + + .icon { + width: 2em; + height: 2em; + margin: 0; + padding: 0; + border-radius: 1em; + } +} + +.certificationIcon { + margin-left: 0.5em; +} + +.certification { background-color: rgba(255, 255, 255, 0.07); -} + margin-right: 0.5em; + margin-top: 1em; + padding: 0.3em 0.6em 0.2em 2.6em; + text-transform: uppercase; -.certification:last-child { - margin-right: 0; -} + &:last-child { + margin-right: 0; + } -.icon { - position: absolute; - top: -.25em; - left: 0; - width: 2em; - height: 2em; - margin: 0; - padding: 0; - border-radius: 1em; + .icon { + position: absolute; + top: -.25em; + left: 0; + } } diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js index 79eaf029e..0693cf2d7 100644 --- a/js/src/ui/Certifications/certifications.js +++ b/js/src/ui/Certifications/certifications.js @@ -28,7 +28,8 @@ class Certifications extends Component { address: PropTypes.string.isRequired, certifications: PropTypes.array.isRequired, className: PropTypes.string, - dappsUrl: PropTypes.string.isRequired + dappsUrl: PropTypes.string.isRequired, + showOnlyIcon: PropTypes.bool } render () { @@ -46,16 +47,47 @@ class Certifications extends Component { } renderCertification = (certification) => { - const { name, title, icon } = certification; - const { dappsUrl } = this.props; + const { name, icon } = certification; + const { dappsUrl, showOnlyIcon } = this.props; - const classNames = `${styles.certification} ${!icon ? styles.noIcon : ''}`; - const img = icon ? dappsUrl + hashToImageUrl(icon) : defaultIcon; + const classNames = [ + showOnlyIcon + ? styles.certificationIcon + : styles.certification, + !icon + ? styles.noIcon + : '' + ]; return ( -
- -
{ title || name }
+
+ + { this.renderCertificationName(certification) } +
+ ); + } + + renderCertificationName = (certification) => { + const { showOnlyIcon } = this.props; + const { name, title } = certification; + + if (showOnlyIcon) { + return null; + } + + return ( +
+ { title || name }
); } diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index c6a9f73ff..6b077e558 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -144,7 +144,10 @@ class Summary extends Component { } />
- { this.renderBalance(true) } +
+ { this.renderBalance(true) } + { this.renderCertifications(true) } +
); } @@ -251,7 +254,7 @@ class Summary extends Component { ); } - renderCertifications () { + renderCertifications (onlyIcon) { const { showCertifications, account } = this.props; if (!showCertifications) { @@ -261,7 +264,12 @@ class Summary extends Component { return ( ); } diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index a8bd97a72..de6ce3ab5 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -41,8 +41,19 @@ margin-top: 1.5em; } - .ethBalances { - opacity: 1; + .summary { + position: relative; + + .ethBalances { + opacity: 1; + } + + .iconCertifications { + bottom: -0.25em; + opacity: 1; + position: absolute; + right: 0; + } } .owners { @@ -71,6 +82,10 @@ .ethBalances { opacity: 0; } + + .iconCertifications { + opacity: 0; + } } } From 54aee9db9b4b9979898d881dff4878320c7d59b9 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Tue, 28 Feb 2017 15:44:30 +0000 Subject: [PATCH 18/93] [ci skip] js-precompiled 20170228-153914 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de9d6e922..5cb5a12f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#d3afc8594280284e0cc75b8db123ec543085ddc0" +source = "git+https://github.com/ethcore/js-precompiled.git#dddb47d1808c73d42916722c6a955c1b37605828" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 8dcbfe263..9090a4bed 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.107", + "version": "0.3.108", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From d436f62eb8d95ee567cc8ba20f2df4467a301d91 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" Date: Wed, 1 Mar 2017 15:57:06 +0400 Subject: [PATCH 19/93] update gitlab-ci docker build beta-release->latest [ci skip] --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84d8638fa..8081e53c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -499,7 +499,7 @@ docker-build: before_script: - docker info script: - - if [ "$CI_BUILD_REF_NAME" == "nightly" ]; then DOCKER_TAG="latest"; else DOCKER_TAG=$CI_BUILD_REF_NAME; fi + - if [ "$CI_BUILD_REF_NAME" == "beta-release" ]; then DOCKER_TAG="latest"; else DOCKER_TAG=$CI_BUILD_REF_NAME; fi - docker login -u $Docker_Hub_User -p $Docker_Hub_Pass - sh scripts/docker-build.sh $DOCKER_TAG tags: From 36468f3fc703db320882ec5776e3f9715767f595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 2 Mar 2017 12:23:54 +0100 Subject: [PATCH 20/93] Removing network=disable from config files (#4715) --- parity/cli/config.full.toml | 1 - parity/cli/config.toml | 1 - parity/cli/mod.rs | 2 -- 3 files changed, 4 deletions(-) diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 60670750b..980457887 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -27,7 +27,6 @@ interface = "127.0.0.1" path = "$HOME/.parity/signer" [network] -disable = false port = 30303 min_peers = 25 max_peers = 50 diff --git a/parity/cli/config.toml b/parity/cli/config.toml index 9b356a811..227578b13 100644 --- a/parity/cli/config.toml +++ b/parity/cli/config.toml @@ -12,7 +12,6 @@ password = ["passwdfile path"] disable = true [network] -disable = false warp = false discovery = true nat = "any" diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 0c057ff30..bbd5e0cff 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -388,7 +388,6 @@ struct Ui { #[derive(Default, Debug, PartialEq, RustcDecodable)] struct Network { - disable: Option, warp: Option, port: Option, min_peers: Option, @@ -838,7 +837,6 @@ mod tests { path: None, }), network: Some(Network { - disable: Some(false), warp: Some(false), port: None, min_peers: Some(10), From 5dd406a19ae4a9c9457f5dd729a812dee319b5be Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 2 Mar 2017 12:24:54 +0100 Subject: [PATCH 21/93] Extract i18n strings in views/* (#4695) * i18n for writecontract * i18n for writecontract store * wallet i18n * wallet confirmations i18n * wallet details i18n * wallet transactions i18n * status i18n * status calls i18n * status callstoolbar i18n * status debug i18n * status editableValue i18n * status miningSettings i18n * status rpcCalls i18n * status rpcDocs i18n * status status i18n * signer i18n * signer origin i18n * signer signRequest i18n * signer transactionMainDetails i18n * sign transactionPending i18n * signer transactionPending i18n * Fix duplicate ids * Typo * Adapt tests for i18n * Actionbar i18n * contracts i18n * contract i18n * contract/queries i18n * contract/events i18n * application/frameError i18n * Actionbar key naming * addresses i18n * address i18n * accounts i18n (tooltip) * Plural strings for owner numbers * IdentityIcon placement * Re-apply s/actiobar/actionbar/ after merge --- js/src/ui/Actionbar/Import/import.js | 16 +- js/src/ui/Actionbar/Sort/sort.js | 50 ++++- js/src/ui/Icons/index.js | 7 + js/src/views/Accounts/Summary/summary.js | 8 +- js/src/views/Address/Delete/delete.js | 18 +- js/src/views/Address/address.js | 45 +++- js/src/views/Addresses/addresses.js | 26 ++- .../Application/FrameError/frameError.js | 6 +- js/src/views/Contract/Events/Event/event.js | 14 +- js/src/views/Contract/Events/events.js | 21 +- js/src/views/Contract/Queries/inputQuery.js | 12 +- js/src/views/Contract/Queries/queries.js | 10 +- js/src/views/Contract/contract.js | 71 ++++-- js/src/views/Contracts/contracts.js | 66 ++++-- .../components/RequestOrigin/requestOrigin.js | 67 +++++- .../RequestOrigin/requestOrigin.spec.js | 12 +- .../components/SignRequest/signRequest.js | 44 +++- .../transactionMainDetails.js | 37 ++- .../TransactionPending/transactionPending.js | 8 +- .../transactionPendingFormConfirm.js | 74 +++++- .../transactionPendingFormReject.js | 21 +- .../transactionPendingForm.js | 22 +- js/src/views/Signer/signer.js | 10 +- js/src/views/Status/components/Calls/Calls.js | 21 +- .../components/CallsToolbar/CallsToolbar.js | 29 ++- js/src/views/Status/components/Debug/debug.js | 38 +++- .../components/EditableValue/EditableValue.js | 26 ++- .../MiningSettings/miningSettings.js | 66 +++++- .../Status/components/RpcCalls/RpcCalls.js | 48 +++- .../Status/components/RpcDocs/RpcDocs.js | 50 ++++- .../views/Status/components/Status/status.js | 115 ++++++++-- js/src/views/Status/status.js | 10 +- .../Wallet/Confirmations/confirmations.js | 65 ++++-- js/src/views/Wallet/Details/details.js | 35 ++- .../views/Wallet/Transactions/transactions.js | 32 ++- js/src/views/Wallet/wallet.js | 83 ++++--- js/src/views/WriteContract/writeContract.js | 211 +++++++++++++++--- .../views/WriteContract/writeContractStore.js | 51 ++++- 38 files changed, 1251 insertions(+), 294 deletions(-) diff --git a/js/src/ui/Actionbar/Import/import.js b/js/src/ui/Actionbar/Import/import.js index ada951cd9..a447be4cb 100644 --- a/js/src/ui/Actionbar/Import/import.js +++ b/js/src/ui/Actionbar/Import/import.js @@ -65,7 +65,7 @@ export default class ActionbarImport extends Component { icon={ } label={ } @@ -87,19 +87,19 @@ export default class ActionbarImport extends Component { const steps = typeof renderValidation === 'function' ? [ , error ? ( ) : ( ) ] @@ -128,7 +128,7 @@ export default class ActionbarImport extends Component { key='cancel' label={ } @@ -147,7 +147,7 @@ export default class ActionbarImport extends Component { key='confirm' label={ } @@ -169,7 +169,7 @@ export default class ActionbarImport extends Component {

diff --git a/js/src/ui/Actionbar/Sort/sort.js b/js/src/ui/Actionbar/Sort/sort.js index 69085d35d..7fd775d12 100644 --- a/js/src/ui/Actionbar/Sort/sort.js +++ b/js/src/ui/Actionbar/Sort/sort.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { observer } from 'mobx-react'; import IconMenu from 'material-ui/IconMenu'; @@ -71,12 +72,38 @@ export default class ActionbarSort extends Component { > { showDefault - ? this.renderMenuItem('', 'Default') - : null + ? this.renderMenuItem('', ( + + )) + : null + } + { + this.renderMenuItem('tags', ( + + )) + } + { + this.renderMenuItem('name', ( + + )) + } + { + this.renderMenuItem('eth', ( + + )) } - { this.renderMenuItem('tags', 'Sort by tags') } - { this.renderMenuItem('name', 'Sort by name') } - { this.renderMenuItem('eth', 'Sort by ETH') } { this.renderSortByMetas() } @@ -88,8 +115,17 @@ export default class ActionbarSort extends Component { return metas .map((meta, index) => { - return this - .renderMenuItem(meta.key, `Sort by ${meta.label}`, index); + const label = ( + + ); + + return this.renderMenuItem(meta.key, label, index); }); } diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index 0ea4f7e0f..442aea92b 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -29,22 +29,29 @@ export ContractIcon from 'material-ui/svg-icons/action/code'; export CopyIcon from 'material-ui/svg-icons/content/content-copy'; export DashboardIcon from 'material-ui/svg-icons/action/dashboard'; export DeleteIcon from 'material-ui/svg-icons/action/delete'; +export DevelopIcon from 'material-ui/svg-icons/action/description'; export DoneIcon from 'material-ui/svg-icons/action/done-all'; export EditIcon from 'material-ui/svg-icons/content/create'; export ErrorIcon from 'material-ui/svg-icons/alert/error'; export FileUploadIcon from 'material-ui/svg-icons/file/file-upload'; export FileIcon from 'material-ui/svg-icons/editor/insert-drive-file'; export FingerprintIcon from 'material-ui/svg-icons/action/fingerprint'; +export GasIcon from 'material-ui/svg-icons/maps/local-gas-station'; export KeyIcon from 'material-ui/svg-icons/communication/vpn-key'; export KeyboardIcon from 'material-ui/svg-icons/hardware/keyboard'; export LinkIcon from 'material-ui/svg-icons/content/link'; +export ListIcon from 'material-ui/svg-icons/action/view-list'; export LockedIcon from 'material-ui/svg-icons/action/lock'; export MembershipIcon from 'material-ui/svg-icons/action/card-membership'; export MoveIcon from 'material-ui/svg-icons/action/open-with'; export NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; +export PauseIcon from 'material-ui/svg-icons/av/pause'; +export PlayIcon from 'material-ui/svg-icons/av/play-arrow'; export PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; export PrintIcon from 'material-ui/svg-icons/action/print'; export RefreshIcon from 'material-ui/svg-icons/action/autorenew'; +export ReorderIcon from 'material-ui/svg-icons/action/reorder'; +export ReplayIcon from 'material-ui/svg-icons/av/replay'; export SaveIcon from 'material-ui/svg-icons/content/save'; export SendIcon from 'material-ui/svg-icons/content/send'; export SettingsIcon from 'material-ui/svg-icons/action/settings'; diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 6b077e558..8b40879c1 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -214,7 +214,13 @@ class Summary extends Component { />
- { owner.name } (owner) + ); diff --git a/js/src/views/Address/Delete/delete.js b/js/src/views/Address/Delete/delete.js index 36d0ea557..8aa5f6111 100644 --- a/js/src/views/Address/Delete/delete.js +++ b/js/src/views/Address/Delete/delete.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -49,13 +50,21 @@ class Delete extends Component { return ( + } visible onDeny={ this.closeDeleteDialog } onConfirm={ this.onDeleteConfirmed } >
- Are you sure you want to remove the following address from your addressbook? +
- +
{ account.address } diff --git a/js/src/views/Address/address.js b/js/src/views/Address/address.js index 3a4f6d5f9..eb0a9bc4b 100644 --- a/js/src/views/Address/address.js +++ b/js/src/views/Address/address.js @@ -15,14 +15,13 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import ActionDelete from 'material-ui/svg-icons/action/delete'; -import ContentCreate from 'material-ui/svg-icons/content/create'; -import ContentAdd from 'material-ui/svg-icons/content/add'; import { EditMeta, AddAddress } from '~/modals'; import { Actionbar, Button, Page } from '~/ui'; +import { AddIcon, DeleteIcon, EditIcon } from '~/ui/Icons'; import Header from '../Account/Header'; import Transactions from '../Account/Transactions'; @@ -146,14 +145,24 @@ class Address extends Component { const buttons = [
); - } catch (e) { return error; } + } catch (e) { + return error; + } } onImport = (content) => { diff --git a/js/src/views/Application/FrameError/frameError.js b/js/src/views/Application/FrameError/frameError.js index c1d022457..0cf1f4ef2 100644 --- a/js/src/views/Application/FrameError/frameError.js +++ b/js/src/views/Application/FrameError/frameError.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component } from 'react'; +import { FormattedMessage } from 'react-intl'; import styles from './frameError.css'; @@ -22,7 +23,10 @@ export default class FrameError extends Component { render () { return (
- ERROR: This application cannot and should not be loaded in an embedded iFrame +
); } diff --git a/js/src/views/Contract/Events/Event/event.js b/js/src/views/Contract/Events/Event/event.js index de5cf6809..5e34e2dd7 100644 --- a/js/src/views/Contract/Events/Event/event.js +++ b/js/src/views/Contract/Events/Event/event.js @@ -17,6 +17,7 @@ import BigNumber from 'bignumber.js'; import moment from 'moment'; import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { IdentityIcon, IdentityName, Input, InputAddress } from '~/ui'; import ShortenedHash from '~/ui/ShortenedHash'; @@ -64,7 +65,12 @@ export default class Event extends Component {
{ event.state === 'pending' - ? 'pending' + ? ( + + ) : this.formatBlockTimestamp(block) }
{ this.formatNumber(transaction.blockNumber) }
@@ -96,7 +102,11 @@ export default class Event extends Component { center inline /> - { withName ? : address } + { + withName + ? + : address + } ); } diff --git a/js/src/views/Contract/Events/events.js b/js/src/views/Contract/Events/events.js index 37fbd0592..1731488d9 100644 --- a/js/src/views/Contract/Events/events.js +++ b/js/src/views/Contract/Events/events.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { uniq } from 'lodash'; import { Container, Loading } from '~/ui'; @@ -22,6 +23,13 @@ import { Container, Loading } from '~/ui'; import Event from './Event'; import styles from '../contract.css'; +const TITLE = ( + +); + export default class Events extends Component { static contextTypes = { api: PropTypes.object @@ -43,7 +51,7 @@ export default class Events extends Component { if (isLoading) { return ( - +
@@ -53,8 +61,13 @@ export default class Events extends Component { if (!events || !events.length) { return ( - -

No events has been sent from this contract.

+ +

+ +

); } @@ -73,7 +86,7 @@ export default class Events extends Component { }); return ( - + diff --git a/js/src/views/Contract/Queries/inputQuery.js b/js/src/views/Contract/Queries/inputQuery.js index 1418aa6b7..a93fe74d0 100644 --- a/js/src/views/Contract/Queries/inputQuery.js +++ b/js/src/views/Contract/Queries/inputQuery.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import LinearProgress from 'material-ui/LinearProgress'; import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card'; import { connect } from 'react-redux'; @@ -80,7 +81,12 @@ class InputQuery extends Component { ); } diff --git a/js/src/views/Status/components/RpcDocs/RpcDocs.js b/js/src/views/Status/components/RpcDocs/RpcDocs.js index a56d182a4..922346763 100644 --- a/js/src/views/Status/components/RpcDocs/RpcDocs.js +++ b/js/src/views/Status/components/RpcDocs/RpcDocs.js @@ -16,6 +16,7 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; +import { FormattedMessage } from 'react-intl'; import { sortBy } from 'lodash'; import List from 'material-ui/List/List'; import ListItem from 'material-ui/List/ListItem'; @@ -38,7 +39,12 @@ class RpcDocs extends Component {
-

RPC Docs

+

+ +

@@ -50,7 +56,12 @@ class RpcDocs extends Component {
+ } className={ styles.autocomplete } dataSource={ rpcMethods.map(m => m.name) } onNewRequest={ this.handleMethodChange } @@ -78,9 +89,38 @@ class RpcDocs extends Component { >

{ m.name }

-

Params{ !m.params.length ? ' - none' : '' }

- { m.params.map((p, idx) => ) } -

Returns -

+

+ + ) + : '' + } } + /> +

+ { + m.params.map((p, idx) => { + return ( + + ); + }) + } +

+ +

{ idx !== rpcMethods.length - 1 ?
: '' } diff --git a/js/src/views/Status/components/Status/status.js b/js/src/views/Status/components/Status/status.js index f7d06833a..efb0c80f7 100644 --- a/js/src/views/Status/components/Status/status.js +++ b/js/src/views/Status/components/Status/status.js @@ -17,6 +17,7 @@ import bytes from 'bytes'; import moment from 'moment'; import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Container, ContainerTitle, Input } from '~/ui'; @@ -47,7 +48,14 @@ export default class Status extends Component {
- + + } + />
#{ nodeStatus.blockNumber.toFormat() }
@@ -56,15 +64,35 @@ export default class Status extends Component {
- + + } + />
{ peers }
- + + } + />
- { `${hashrate} H/s` } +
@@ -89,7 +117,12 @@ export default class Status extends Component { return ( - { nodeStatus.nodeName || 'Node' } + { nodeStatus.nodeName || ( + ) + } ); } @@ -107,11 +140,23 @@ export default class Status extends Component { return (
- + + } + /> + } value={ nodeStatus.netChain } { ...this._test('chain') } /> @@ -120,7 +165,12 @@ export default class Status extends Component { + } value={ peers } { ...this._test('peers') } /> @@ -129,7 +179,12 @@ export default class Status extends Component { + } value={ netPort.toString() } { ...this._test('network-port') } /> @@ -139,11 +194,26 @@ export default class Status extends Component { + } value={ rpcSettings.enabled - ? 'yes' - : 'no' + ? ( + + ) + : ( + + ) } { ...this._test('rpc-enabled') } /> @@ -152,7 +222,12 @@ export default class Status extends Component { + } value={ rpcSettings.interface } { ...this._test('rpc-interface') } /> @@ -161,7 +236,12 @@ export default class Status extends Component { + } value={ rpcPort.toString() } { ...this._test('rpc-port') } /> @@ -173,7 +253,12 @@ export default class Status extends Component { + } value={ nodeStatus.enode } { ...this._test('node-enode') } /> diff --git a/js/src/views/Status/status.js b/js/src/views/Status/status.js index ff17bd575..68621fe9b 100644 --- a/js/src/views/Status/status.js +++ b/js/src/views/Status/status.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Page } from '~/ui'; @@ -23,7 +24,14 @@ import StatusPage from './containers/StatusPage'; export default class Status extends Component { render () { return ( - + + } + > ); diff --git a/js/src/views/Wallet/Confirmations/confirmations.js b/js/src/views/Wallet/Confirmations/confirmations.js index fd4d65a12..8f7e6ea52 100644 --- a/js/src/views/Wallet/Confirmations/confirmations.js +++ b/js/src/views/Wallet/Confirmations/confirmations.js @@ -14,8 +14,9 @@ // 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 { LinearProgress, MenuItem, IconMenu } from 'material-ui'; +import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import ReactTooltip from 'react-tooltip'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -70,7 +71,12 @@ class WalletConfirmations extends Component { if (realConfirmations.length === 0) { return (
-

No transactions needs confirmation right now.

+

+ +

); } @@ -217,7 +223,12 @@ class WalletConfirmation extends Component { const confirmButton = (
+ - @@ -326,27 +359,27 @@ class WalletConfirmation extends Component { if (value && to && data) { return ( ); } return (
@@ -263,11 +282,15 @@ class WalletConfirmation extends Component { const account = this.props.accounts[address]; return ( - +
{ account.name.toUpperCase() || account.address }
@@ -283,7 +306,10 @@ class WalletConfirmation extends Component { return (
+
- Confirmed by { confirmedBy.length }/{ require.toNumber() } owners +
{ operation } diff --git a/js/src/views/Wallet/Details/details.js b/js/src/views/Wallet/Details/details.js index 0fe7b9257..37e3e9395 100644 --- a/js/src/views/Wallet/Details/details.js +++ b/js/src/views/Wallet/Details/details.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Container, InputAddress } from '~/ui'; @@ -40,7 +41,14 @@ export default class WalletDetails extends Component { return (
- + + } + > { this.renderDetails() } { this.renderOwners() } @@ -62,10 +70,10 @@ export default class WalletDetails extends Component { return ( ); }); @@ -87,9 +95,24 @@ export default class WalletDetails extends Component { return (

- This wallet requires at least - { require.toFormat() } owners - to validate any action (transactions, modifications). + + + + ) + } } + />

); diff --git a/js/src/views/Wallet/Transactions/transactions.js b/js/src/views/Wallet/Transactions/transactions.js index 35d8c03f1..d81c2633a 100644 --- a/js/src/views/Wallet/Transactions/transactions.js +++ b/js/src/views/Wallet/Transactions/transactions.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { bytesToHex } from '~/api/util/format'; import { Container } from '~/ui'; @@ -36,7 +37,14 @@ export default class WalletTransactions extends Component { render () { return (
- + + } + > { this.renderTransactions() }
@@ -52,7 +60,12 @@ export default class WalletTransactions extends Component { if (transactions.length === 0) { return (
-

No transactions has been sent.

+

+ +

); } @@ -62,14 +75,17 @@ export default class WalletTransactions extends Component { return ( ); }); diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js index 55e15dbc9..77f938a78 100644 --- a/js/src/views/Wallet/wallet.js +++ b/js/src/views/Wallet/wallet.js @@ -15,18 +15,15 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import moment from 'moment'; -import ContentCreate from 'material-ui/svg-icons/content/create'; -import ActionDelete from 'material-ui/svg-icons/action/delete'; -import ContentSend from 'material-ui/svg-icons/content/send'; -import SettingsIcon from 'material-ui/svg-icons/action/settings'; - -import { nullableProptype } from '~/util/proptypes'; import { EditMeta, Transfer, WalletSettings } from '~/modals'; import { Actionbar, Button, Page, Loading } from '~/ui'; +import { DeleteIcon, EditIcon, SendIcon, SettingsIcon } from '~/ui/Icons'; +import { nullableProptype } from '~/util/proptypes'; import Delete from '../Address/Delete'; import Header from '../Account/Header'; @@ -53,7 +50,10 @@ class WalletContainer extends Component { } return ( - + ); } } @@ -166,11 +166,15 @@ class Wallet extends Component {

- { spent } - has been spent today, out of - { limit } - set as the daily limit, which has been reset on - { date.format('LL') } + { date.format('LL') }, + limit: { limit }, + spent: { spent } + } } + />

); @@ -190,19 +194,19 @@ class Wallet extends Component { return [ , ]; } @@ -216,10 +220,15 @@ class Wallet extends Component { if (owned) { buttons.push(
@@ -146,7 +146,12 @@ class WriteContract extends Component { const { selectedContract } = this.store; if (!selectedContract || !selectedContract.name) { - return 'New Solidity Contract'; + return ( + + ); } return ( @@ -154,9 +159,23 @@ class WriteContract extends Component { { selectedContract.name } + } > - (saved { moment(selectedContract.timestamp).fromNow() }) + ); @@ -176,20 +195,35 @@ class WriteContract extends Component { const buttons = [
-
+
{ verb } @@ -80,17 +86,23 @@ const renderDataChanged = (e) => { return (
-
+
updated - { 'key ' } + key  { new Buffer(e.parameters.plainKey.value).toString('utf8') } - { 'of ' } +  of  diff --git a/js/src/dapps/registry/actions.js b/js/src/dapps/registry/actions.js index d56c76f4b..a47034942 100644 --- a/js/src/dapps/registry/actions.js +++ b/js/src/dapps/registry/actions.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { registry as registryAbi } from '~/contracts/abi'; +import { registry as registryAbi, registry2 as registryAbi2 } from '~/contracts/abi'; import { api } from './parity.js'; import * as addresses from './addresses/actions.js'; @@ -27,6 +27,11 @@ import * as reverse from './Reverse/actions.js'; export { addresses, accounts, lookup, events, names, records, reverse }; +const REGISTRY_V1_HASHES = [ + '0x34f7c51bbb1b1902fbdabfdf04811100f5c9f998f26dd535d2f6f977492c748e', // ropsten + '0x64c3ee34851517a9faecd995c102b339f03e564ad6772dc43a26f993238b20ec' // homestead +]; + export const setIsTestnet = (isTestnet) => ({ type: 'set isTestnet', isTestnet }); export const fetchIsTestnet = () => (dispatch) => @@ -47,13 +52,28 @@ export const fetchIsTestnet = () => (dispatch) => export const setContract = (contract) => ({ type: 'set contract', contract }); export const fetchContract = () => (dispatch) => - api.parity.registryAddress() + api.parity + .registryAddress() .then((address) => { - const contract = api.newContract(registryAbi, address); + return api.eth + .getCode(address) + .then((code) => { + const codeHash = api.util.sha3(code); + const isVersion1 = REGISTRY_V1_HASHES.includes(codeHash); - dispatch(setContract(contract)); - dispatch(fetchFee()); - dispatch(fetchOwner()); + console.log(`registry at ${address}, code ${codeHash}, version ${isVersion1 ? 1 : 2}`); + + const contract = api.newContract( + isVersion1 + ? registryAbi + : registryAbi2, + address + ); + + dispatch(setContract(contract)); + dispatch(fetchFee()); + dispatch(fetchOwner()); + }); }) .catch((err) => { console.error('could not fetch contract'); diff --git a/parity/params.rs b/parity/params.rs index d9aee81cd..85019b3e7 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -26,7 +26,7 @@ use user_defaults::UserDefaults; #[derive(Debug, PartialEq)] pub enum SpecType { - Mainnet, + Foundation, Morden, Ropsten, Kovan, @@ -39,7 +39,7 @@ pub enum SpecType { impl Default for SpecType { fn default() -> Self { - SpecType::Mainnet + SpecType::Foundation } } @@ -48,7 +48,7 @@ impl str::FromStr for SpecType { fn from_str(s: &str) -> Result { let spec = match s { - "frontier" | "homestead" | "mainnet" => SpecType::Mainnet, + "foundation" | "frontier" | "homestead" | "mainnet" => SpecType::Foundation, "frontier-dogmatic" | "homestead-dogmatic" | "classic" => SpecType::Classic, "morden" | "classic-testnet" => SpecType::Morden, "ropsten" => SpecType::Ropsten, @@ -65,7 +65,7 @@ impl str::FromStr for SpecType { impl fmt::Display for SpecType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match *self { - SpecType::Mainnet => "homestead", + SpecType::Foundation => "foundation", SpecType::Morden => "morden", SpecType::Ropsten => "ropsten", SpecType::Olympic => "olympic", @@ -81,7 +81,7 @@ impl fmt::Display for SpecType { impl SpecType { pub fn spec(&self) -> Result { match *self { - SpecType::Mainnet => Ok(ethereum::new_frontier()), + SpecType::Foundation => Ok(ethereum::new_foundation()), SpecType::Morden => Ok(ethereum::new_morden()), SpecType::Ropsten => Ok(ethereum::new_ropsten()), SpecType::Olympic => Ok(ethereum::new_olympic()), @@ -321,9 +321,10 @@ mod tests { #[test] fn test_spec_type_parsing() { - assert_eq!(SpecType::Mainnet, "frontier".parse().unwrap()); - assert_eq!(SpecType::Mainnet, "homestead".parse().unwrap()); - assert_eq!(SpecType::Mainnet, "mainnet".parse().unwrap()); + assert_eq!(SpecType::Foundation, "frontier".parse().unwrap()); + assert_eq!(SpecType::Foundation, "homestead".parse().unwrap()); + assert_eq!(SpecType::Foundation, "mainnet".parse().unwrap()); + assert_eq!(SpecType::Foundation, "foundation".parse().unwrap()); assert_eq!(SpecType::Kovan, "testnet".parse().unwrap()); assert_eq!(SpecType::Kovan, "kovan".parse().unwrap()); assert_eq!(SpecType::Morden, "morden".parse().unwrap()); @@ -335,12 +336,12 @@ mod tests { #[test] fn test_spec_type_default() { - assert_eq!(SpecType::Mainnet, SpecType::default()); + assert_eq!(SpecType::Foundation, SpecType::default()); } #[test] fn test_spec_type_display() { - assert_eq!(format!("{}", SpecType::Mainnet), "homestead"); + assert_eq!(format!("{}", SpecType::Foundation), "foundation"); assert_eq!(format!("{}", SpecType::Ropsten), "ropsten"); assert_eq!(format!("{}", SpecType::Morden), "morden"); assert_eq!(format!("{}", SpecType::Olympic), "olympic"); From fd19f6f4493142e8f571d87050358583d582a115 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 3 Mar 2017 13:07:34 +0000 Subject: [PATCH 44/93] [ci skip] js-precompiled 20170303-130222 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df013f9a5..18cf2315e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#8dedb98fcbf02a8345553c7d7a1fe654822f220d" +source = "git+https://github.com/ethcore/js-precompiled.git#df288669705e7f5c970ab532ae920af906be7727" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 489149682..9cf066856 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.112", + "version": "0.3.113", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From 55593746764d499dbd34ae54fc29cd5192995888 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 3 Mar 2017 14:32:52 +0100 Subject: [PATCH 45/93] Fix Account Selection in Signer (#4744) * Can pass FormattedMessage to Input (eg. Status // RPC Enabled) * Simple fixed-width fix for Accoutn Selection in Parity Signer --- js/src/ui/Form/Input/input.js | 16 ++++++++++++++-- js/src/views/ParityBar/parityBar.css | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index 9491bb2d3..44906ad93 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -46,6 +46,10 @@ const UNDERLINE_FOCUSED = { const NAME_ID = ' '; export default class Input extends Component { + static contextTypes = { + intl: React.PropTypes.object.isRequired + }; + static propTypes = { allowCopy: PropTypes.oneOfType([ PropTypes.string, @@ -79,7 +83,8 @@ export default class Input extends Component { style: PropTypes.object, value: PropTypes.oneOfType([ PropTypes.number, - PropTypes.string + PropTypes.string, + PropTypes.node ]) }; @@ -135,6 +140,13 @@ export default class Input extends Component { ? UNDERLINE_FOCUSED : readOnly && typeof focused !== 'boolean' ? { display: 'none' } : null; + const textValue = typeof value !== 'string' && (value && value.props) + ? this.context.intl.formatMessage( + value.props, + value.props.values || {} + ) + : value; + return (
{ this.renderCopyButton() } @@ -169,7 +181,7 @@ export default class Input extends Component { underlineStyle={ underlineStyle } underlineFocusStyle={ underlineFocusStyle } underlineShow={ !hideUnderline } - value={ value } + value={ textValue } > { children } diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index 265bf7894..11ebce073 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -42,6 +42,7 @@ $modalZ: 10001; .container { display: flex; flex-direction: column; + width: 100%; } .overlay { @@ -106,6 +107,7 @@ $modalZ: 10001; min-height: 30vh; max-height: 80vh; max-width: calc(100vw - 2em); + width: 960px; .content { flex: 1; From 6760ae0e84b614b496bf73fab30970ee824f49d6 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Mar 2017 14:38:40 +0100 Subject: [PATCH 46/93] Account selector close operations (#4728) * Close when clicking anywhere on body pane * Allow click to propagate on address click * Rename noCopy -> allowAddressClick * Handle i18n strings in input * Close on pasted addresses (valid entry) * allowAddressClick default * Don't do onClick on AccountCard if text is selected * Reset filter value on close for address selection * Better close on click for AccountSelection --- js/src/ui/AccountCard/accountCard.js | 31 ++++++++++++++++--- js/src/ui/Form/AddressSelect/addressSelect.js | 13 ++++++++ .../InputAddressSelect/inputAddressSelect.js | 8 +++-- js/src/ui/Portal/portal.js | 13 +++++++- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index 6a6ebb22c..d7b455132 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -28,12 +28,17 @@ import styles from './accountCard.css'; export default class AccountCard extends Component { static propTypes = { account: PropTypes.object.isRequired, + allowAddressClick: PropTypes.bool, balance: PropTypes.object, className: PropTypes.string, onClick: PropTypes.func, onFocus: PropTypes.func }; + static defaultProps = { + allowAddressClick: false + }; + state = { copied: false }; @@ -122,7 +127,7 @@ export default class AccountCard extends Component {
@@ -132,6 +137,17 @@ export default class AccountCard extends Component { ); } + handleAddressClick = (event) => { + const { allowAddressClick } = this.props; + + // Don't stop the event if address click is allowed + if (allowAddressClick) { + return this.onClick(event); + } + + return this.preventEvent(event); + } + handleKeyDown = (event) => { const codeName = keycode(event); @@ -172,9 +188,14 @@ export default class AccountCard extends Component { } } - onClick = () => { + onClick = (event) => { const { account, onClick } = this.props; + // Stop the default event if text is selected + if (window.getSelection && window.getSelection().type === 'Range') { + return this.preventEvent(event); + } + onClick && onClick(account.address); } @@ -184,9 +205,9 @@ export default class AccountCard extends Component { onFocus && onFocus(account.index); } - preventEvent = (e) => { - e.preventDefault(); - e.stopPropagation(); + preventEvent = (event) => { + event.preventDefault(); + event.stopPropagation(); } setTagRef = (tagRef) => { diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 07c133d7c..785a8fa65 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -23,6 +23,7 @@ import { observer } from 'mobx-react'; import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline'; +import apiutil from '~/api/util'; import AccountCard from '~/ui/AccountCard'; import InputAddress from '~/ui/Form/InputAddress'; import Loading from '~/ui/Loading'; @@ -177,6 +178,7 @@ class AddressSelect extends Component { { + event.stopPropagation(); + } + handleInputAddresKeydown = (event) => { const code = keycode(event); @@ -588,6 +596,7 @@ class AddressSelect extends Component { } this.store.resetRegistryValues(); + this.store.handleChange(''); this.setState({ expanded: false, @@ -613,6 +622,10 @@ class AddressSelect extends Component { focusedItem: null, inputValue: value }); + + if (apiutil.isAddressValid(value)) { + this.handleClick(value); + } } } diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js index c6c3fe6a4..8199fece6 100644 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js +++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js @@ -17,6 +17,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; +import { nodeOrStringProptype } from '~/util/proptypes'; + import AddressSelect from '../AddressSelect'; class InputAddressSelect extends Component { @@ -27,9 +29,9 @@ class InputAddressSelect extends Component { allowCopy: PropTypes.bool, className: PropTypes.string, - error: PropTypes.string, - hint: PropTypes.string, - label: PropTypes.string, + error: nodeOrStringProptype(), + hint: nodeOrStringProptype(), + label: nodeOrStringProptype(), onChange: PropTypes.func, readOnly: PropTypes.bool, value: PropTypes.string diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index 213b2f7a9..9d270b0cc 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -44,6 +44,7 @@ export default class Portal extends Component { hideClose: PropTypes.bool, isChildModal: PropTypes.bool, isSmallModal: PropTypes.bool, + onClick: PropTypes.func, onKeyDown: PropTypes.func, steps: PropTypes.array, title: nodeOrStringProptype() @@ -89,7 +90,7 @@ export default class Portal extends Component { className ].join(' ') } - onClick={ this.stopEvent } + onClick={ this.handleContainerClick } onKeyDown={ this.handleKeyDown } > { + const { onClick } = this.props; + + if (!onClick) { + return this.stopEvent(event); + } + + return onClick(event); + } + handleClose = () => { const { hideClose, onClose } = this.props; From 4024100e7400d28e8f39346ab46d22ed6440a047 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 3 Mar 2017 13:53:17 +0000 Subject: [PATCH 47/93] [ci skip] js-precompiled 20170303-134752 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18cf2315e..e743c026a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#df288669705e7f5c970ab532ae920af906be7727" +source = "git+https://github.com/ethcore/js-precompiled.git#71671e74a18a5a324dfabe8d3472bc349cb6c9b3" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 9cf066856..1ba15e8a6 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.113", + "version": "0.3.114", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From d297ddbfe56c743e1c6f4a3993e7bf804e931ae0 Mon Sep 17 00:00:00 2001 From: Maciej Hirsz Date: Fri, 3 Mar 2017 14:55:04 +0100 Subject: [PATCH 48/93] Update wiki (#4743) - Added missing `creates` field. - Removed deprecated methods. --- js/src/jsonrpc/interfaces/eth.js | 58 ------------------- js/src/jsonrpc/interfaces/parity.js | 86 +--------------------------- js/src/jsonrpc/types.js | 89 +++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 141 deletions(-) diff --git a/js/src/jsonrpc/interfaces/eth.js b/js/src/jsonrpc/interfaces/eth.js index 14003f4f2..6489a62ac 100644 --- a/js/src/jsonrpc/interfaces/eth.js +++ b/js/src/jsonrpc/interfaces/eth.js @@ -97,54 +97,6 @@ The following options are possible for the \`defaultBlock\` parameter: } }, - compileSerpent: { - desc: 'Returns compiled serpent code.', - params: [ - { - type: String, - desc: 'The source code.', - example: '/* some serpent */' - } - ], - returns: { - type: Data, - desc: 'The compiled source code.', - example: '0x603880600c6000396000f3006001600060e060020a600035048063c6888fa114601857005b6021600435602b565b8060005260206000f35b600081600702905091905056' - } - }, - - compileSolidity: { - desc: 'Returns compiled solidity code.', - params: [ - { - type: String, - desc: 'The source code.', - example: 'contract test { function multiply(uint a) returns(uint d) { return a * 7; } }' - } - ], - returns: { - type: Data, - desc: 'The compiled source code.', - example: '0x605880600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b603d6004803590602001506047565b8060005260206000f35b60006007820290506053565b91905056' - } - }, - - compileLLL: { - desc: 'Returns compiled LLL code.', - params: [ - { - type: String, - desc: 'The source code.', - example: '(returnlll (suicide (caller)))' - } - ], - returns: { - type: Data, - desc: 'The compiled source code.', - example: '0x603880600c6000396000f3006001600060e060020a600035048063c6888fa114601857005b6021600435602b565b8060005260206000f35b600081600702905091905056' - } - }, - estimateGas: { desc: 'Makes a call or transaction, which won\'t be added to the blockchain and returns the used gas, which can be used for estimating the used gas.', params: [ @@ -415,16 +367,6 @@ The following options are possible for the \`defaultBlock\` parameter: } }, - getCompilers: { - desc: 'Returns a list of available compilers in the client.', - params: [], - returns: { - type: Array, - desc: 'Array of available compilers.', - example: ['solidity', 'lll', 'serpent'] - } - }, - getFilterChanges: { desc: 'Polling method for a filter, which returns an array of logs which occurred since last poll.', params: [ diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js index 8aca8312f..cf62213a5 100644 --- a/js/src/jsonrpc/interfaces/parity.js +++ b/js/src/jsonrpc/interfaces/parity.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { Address, Data, Hash, Quantity, BlockNumber, TransactionRequest } from '../types'; +import { Address, Data, Hash, Quantity, BlockNumber, TransactionRequest, TransactionResponse } from '../types'; import { fromDecimal, withComment, Dummy } from '../helpers'; const SECTION_ACCOUNTS = 'Accounts (read-only) and Signatures'; @@ -27,86 +27,6 @@ const SECTION_VAULT = 'Account Vaults'; const SUBDOC_SET = 'set'; const SUBDOC_ACCOUNTS = 'accounts'; -const transactionDetails = { - hash: { - type: Hash, - desc: '32 Bytes - hash of the transaction.' - }, - nonce: { - type: Quantity, - desc: 'The number of transactions made by the sender prior to this one.' - }, - blockHash: { - type: Hash, - desc: '32 Bytes - hash of the block where this transaction was in. `null` when its pending.' - }, - blockNumber: { - type: BlockNumber, - desc: 'Block number where this transaction was in. `null` when its pending.' - }, - transactionIndex: { - type: Quantity, - desc: 'Integer of the transactions index position in the block. `null` when its pending.' - }, - from: { - type: Address, - desc: '20 Bytes - address of the sender.' - }, - to: { - type: Address, - desc: '20 Bytes - address of the receiver. `null` when its a contract creation transaction.' - }, - value: { - type: Quantity, - desc: 'Value transferred in Wei.' - }, - gasPrice: { - type: Quantity, - desc: 'Gas price provided by the sender in Wei.' - }, - gas: { - type: Quantity, - desc: 'Gas provided by the sender.' - }, - input: { - type: Data, - desc: 'The data send along with the transaction.' - }, - raw: { - type: Data, - desc: 'Raw transaction data.' - }, - publicKey: { - type: Data, - desc: 'Public key of the signer.' - }, - networkId: { - type: Quantity, - desc: 'The network id of the transaction, if any.' - }, - standardV: { - type: Quantity, - desc: 'The standardized V field of the signature (0 or 1).' - }, - v: { - type: Quantity, - desc: 'The V field of the signature.' - }, - r: { - type: Quantity, - desc: 'The R field of the signature.' - }, - s: { - type: Quantity, - desc: 'The S field of the signature.' - }, - condition: { - type: Object, - optional: true, - desc: 'Conditional submission, Block number in `block` or timestamp in `time` or `null`.' - } -}; - export default { accountsInfo: { section: SECTION_ACCOUNTS, @@ -634,7 +554,7 @@ export default { returns: { type: Array, desc: 'Transactions ordered by priority', - details: transactionDetails, + details: TransactionResponse.details, example: [ { blockHash: null, @@ -950,7 +870,7 @@ export default { returns: { type: Array, desc: 'Transaction list.', - details: transactionDetails, + details: TransactionResponse.details, example: [ { hash: '0x80de421cd2e7e46824a91c343ca42b2ff339409eef09e2d9d73882462f8fce31', diff --git a/js/src/jsonrpc/types.js b/js/src/jsonrpc/types.js index 4f5a085ce..8803fdd5c 100644 --- a/js/src/jsonrpc/types.js +++ b/js/src/jsonrpc/types.js @@ -109,3 +109,92 @@ export class TransactionRequest { } } } + +export class TransactionResponse { + static print = '`Object`'; + + static details = { + hash: { + type: Hash, + desc: '32 Bytes - hash of the transaction.' + }, + nonce: { + type: Quantity, + desc: 'The number of transactions made by the sender prior to this one.' + }, + blockHash: { + type: Hash, + desc: '32 Bytes - hash of the block where this transaction was in. `null` when its pending.' + }, + blockNumber: { + type: BlockNumber, + desc: 'Block number where this transaction was in. `null` when its pending.' + }, + transactionIndex: { + type: Quantity, + desc: 'Integer of the transactions index position in the block. `null` when its pending.' + }, + from: { + type: Address, + desc: '20 Bytes - address of the sender.' + }, + to: { + type: Address, + desc: '20 Bytes - address of the receiver. `null` when its a contract creation transaction.' + }, + value: { + type: Quantity, + desc: 'Value transferred in Wei.' + }, + gasPrice: { + type: Quantity, + desc: 'Gas price provided by the sender in Wei.' + }, + gas: { + type: Quantity, + desc: 'Gas provided by the sender.' + }, + input: { + type: Data, + desc: 'The data send along with the transaction.' + }, + creates: { + type: Address, + optional: true, + desc: 'Address of a created contract or `null`.' + }, + raw: { + type: Data, + desc: 'Raw transaction data.' + }, + publicKey: { + type: Data, + desc: 'Public key of the signer.' + }, + networkId: { + type: Quantity, + desc: 'The network id of the transaction, if any.' + }, + standardV: { + type: Quantity, + desc: 'The standardized V field of the signature (0 or 1).' + }, + v: { + type: Quantity, + desc: 'The V field of the signature.' + }, + r: { + type: Quantity, + desc: 'The R field of the signature.' + }, + s: { + type: Quantity, + desc: 'The S field of the signature.' + }, + condition: { + type: Object, + optional: true, + desc: 'Conditional submission, Block number in `block` or timestamp in `time` or `null`.' + } + } +} From ead40a8b9711b1b78799dcdc0bb5dc4f4d8d47af Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 3 Mar 2017 14:07:22 +0000 Subject: [PATCH 49/93] [ci skip] js-precompiled 20170303-140210 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e743c026a..605ffa8a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#71671e74a18a5a324dfabe8d3472bc349cb6c9b3" +source = "git+https://github.com/ethcore/js-precompiled.git#17f9c870020a896bf6a52e00be5cdf6ebd912b1b" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 1ba15e8a6..0926363c6 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.114", + "version": "0.3.115", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From ec0e8f9dd681b683b8fb51f14c5ec0e74c24ff54 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 3 Mar 2017 19:49:36 +0100 Subject: [PATCH 50/93] Add StackEventListener (#4745) --- js/src/ui/Portal/portal.js | 9 +-- js/src/ui/StackEventListener/index.js | 17 ++++++ .../StackEventListener/stackEventListener.js | 56 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 js/src/ui/StackEventListener/index.js create mode 100644 js/src/ui/StackEventListener/stackEventListener.js diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index 9d270b0cc..f4c64bf46 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import EventListener from 'react-event-listener'; import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import ReactPortal from 'react-portal'; @@ -23,6 +22,7 @@ import keycode from 'keycode'; import { nodeOrStringProptype } from '~/util/proptypes'; import { CloseIcon } from '~/ui/Icons'; import ParityBackground from '~/ui/ParityBackground'; +import StackEventListener from '~/ui/StackEventListener'; import Title from '~/ui/Title'; import styles from './portal.css'; @@ -93,10 +93,7 @@ export default class Portal extends Component { onClick={ this.handleContainerClick } onKeyDown={ this.handleKeyDown } > - + { this.renderClose() } . + +export default from './stackEventListener'; diff --git a/js/src/ui/StackEventListener/stackEventListener.js b/js/src/ui/StackEventListener/stackEventListener.js new file mode 100644 index 000000000..586ddcad6 --- /dev/null +++ b/js/src/ui/StackEventListener/stackEventListener.js @@ -0,0 +1,56 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import ReactEventListener from 'react-event-listener'; +import React, { Component, PropTypes } from 'react'; + +let listenerId = 0; +let listenerIds = []; + +export default class StackEventListener extends Component { + static propTypes = { + onKeyUp: PropTypes.func.isRequired + }; + + componentWillMount () { + // Add to the list of listeners on mount + this.id = ++listenerId; + listenerIds.push(this.id); + } + + componentWillUnmount () { + // Remove from the listeners list on unmount + listenerIds = listenerIds.filter((id) => this.id !== id); + } + + render () { + return ( + <ReactEventListener + target='window' + onKeyUp={ this.handleKeyUp } + /> + ); + } + + handleKeyUp = (event) => { + // Only handle event if last of the listeners list + if (this.id !== listenerIds.slice(-1)[0]) { + return event; + } + + return this.props.onKeyUp(event); + } +} From 9b6170a37b4f22f775ffb3632fbee65f42591e25 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Fri, 3 Mar 2017 19:49:46 +0100 Subject: [PATCH 51/93] Update testnet detection (#4746) --- js/src/dapps/registry/actions.js | 9 +++++---- js/src/dapps/tokendeploy/services.js | 2 +- js/src/redux/providers/status.js | 8 +++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/js/src/dapps/registry/actions.js b/js/src/dapps/registry/actions.js index a47034942..2346cf323 100644 --- a/js/src/dapps/registry/actions.js +++ b/js/src/dapps/registry/actions.js @@ -37,10 +37,11 @@ export const setIsTestnet = (isTestnet) => ({ type: 'set isTestnet', isTestnet } export const fetchIsTestnet = () => (dispatch) => api.net.version() .then((netVersion) => { - dispatch(setIsTestnet( - netVersion === '2' || // morden - netVersion === '3' // ropsten - )); + dispatch(setIsTestnet([ + '2', // morden + '3', // ropsten + '42' // kovan + ].includes(netVersion))); }) .catch((err) => { console.error('could not check if testnet'); diff --git a/js/src/dapps/tokendeploy/services.js b/js/src/dapps/tokendeploy/services.js index 6853c8ac4..1285371aa 100644 --- a/js/src/dapps/tokendeploy/services.js +++ b/js/src/dapps/tokendeploy/services.js @@ -122,7 +122,7 @@ export function attachInstances () { .then(([registryAddress, netChain]) => { const registry = api.newContract(abis.registry, registryAddress).instance; - isTest = ['morden', 'ropsten', 'testnet'].includes(netChain); + isTest = ['kovan', 'morden', 'ropsten', 'testnet'].includes(netChain); console.log(`contract was found at registry=${registryAddress}`); console.log(`running on ${netChain}, isTest=${isTest}`); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index e2e1e61f9..4cf8ea186 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -290,9 +290,11 @@ export default class Status { .then(([ netPeers, clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode, upgradeStatus ]) => { - const isTest = - netVersion === '2' || // morden - netVersion === '3'; // ropsten + const isTest = [ + '2', // morden + '3', // ropsten + '42' // kovan + ].includes(netVersion); const longStatus = { netPeers, From cb118f1936a724ed2a72108fd987fca8d3af2de3 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Fri, 3 Mar 2017 19:50:25 +0100 Subject: [PATCH 52/93] Update SelectionList indicators (#4736) * Move selector indicators to the left * Only display default account selection icon --- .../DappPermissions/dappPermissions.css | 25 ---------------- .../modals/DappPermissions/dappPermissions.js | 15 ---------- js/src/ui/SelectionList/selectionList.css | 29 +++++++++++++++---- js/src/ui/SelectionList/selectionList.js | 23 +++++++-------- 4 files changed, 34 insertions(+), 58 deletions(-) delete mode 100644 js/src/modals/DappPermissions/dappPermissions.css diff --git a/js/src/modals/DappPermissions/dappPermissions.css b/js/src/modals/DappPermissions/dappPermissions.css deleted file mode 100644 index 44d901572..000000000 --- a/js/src/modals/DappPermissions/dappPermissions.css +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. -*/ - -.legend { - opacity: 0.75; - - span { - line-height: 24px; - vertical-align: top; - } -} diff --git a/js/src/modals/DappPermissions/dappPermissions.js b/js/src/modals/DappPermissions/dappPermissions.js index ddb4b040d..903e31fd9 100644 --- a/js/src/modals/DappPermissions/dappPermissions.js +++ b/js/src/modals/DappPermissions/dappPermissions.js @@ -20,9 +20,6 @@ import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { AccountCard, Portal, SelectionList } from '~/ui'; -import { CheckIcon, StarIcon } from '~/ui/Icons'; - -import styles from './dappPermissions.css'; @observer class DappPermissions extends Component { @@ -40,18 +37,6 @@ class DappPermissions extends Component { return ( <Portal - buttons={ - <div className={ styles.legend }> - <FormattedMessage - id='dapps.permissions.description' - defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account' - values={ { - activeIcon: <CheckIcon />, - defaultIcon: <StarIcon /> - } } - /> - </div> - } onClose={ permissionStore.closeModal } open title={ diff --git a/js/src/ui/SelectionList/selectionList.css b/js/src/ui/SelectionList/selectionList.css index f12b10af2..44f5a39b3 100644 --- a/js/src/ui/SelectionList/selectionList.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -40,8 +40,29 @@ .overlay { position: absolute; - right: 0.5em; - top: 0.5em; + left: 0.75em; + top: 0.75em; + z-index: 1; + + .icon, + .iconDisabled { + border-radius: 0.25em; + color: #333 !important; + cursor: pointer; + margin-right: 0.25em; + } + + .icon { + background: white; + } + + .iconDisabled { + background: #666; + + &:hover { + background: white; + } + } } } @@ -58,7 +79,3 @@ filter: grayscale(10%); opacity: 0.75; } - -.iconDisabled { - opacity: 0.15; -} diff --git a/js/src/ui/SelectionList/selectionList.js b/js/src/ui/SelectionList/selectionList.js index 1e38d39b0..b8ee1fee6 100644 --- a/js/src/ui/SelectionList/selectionList.js +++ b/js/src/ui/SelectionList/selectionList.js @@ -16,7 +16,7 @@ import React, { Component, PropTypes } from 'react'; -import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; +import { StarIcon } from '~/ui/Icons'; import SectionList from '~/ui/SectionList'; import { arrayOrObjectProptype } from '~/util/proptypes'; @@ -62,9 +62,15 @@ export default class SelectionList extends Component { let defaultIcon = null; if (onDefaultClick) { - defaultIcon = isSelected && item.default - ? <StarIcon /> - : <StarOutlineIcon className={ styles.iconDisabled } onClick={ makeDefault } />; + defaultIcon = ( + <div className={ styles.overlay }> + { + isSelected && item.default + ? <StarIcon className={ styles.icon } /> + : <StarIcon className={ styles.iconDisabled } onClick={ makeDefault } /> + } + </div> + ); } const classes = isSelected @@ -83,14 +89,7 @@ export default class SelectionList extends Component { > { renderItem(item, index) } </div> - <div className={ styles.overlay }> - { defaultIcon } - { - isSelected - ? <CheckIcon onClick={ selectItem } /> - : <CheckIcon className={ styles.iconDisabled } onClick={ selectItem } /> - } - </div> + { defaultIcon } </div> ); } From 1548201551f36c1cb4383c536844b389a0bf98fd Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Fri, 3 Mar 2017 19:50:54 +0100 Subject: [PATCH 53/93] Vault Management UI (round 3) (#4652) * Render Dapps via SectionList * Initial rendering of accounts via SectionList * Width vars * Allow classNames in certifications & tags * Overlay of info on hover * Adjust hover balances * Large owner icons (align with vaults) * Consistent block mined at message * Attach ParityBackground to html * Adjust page padding to align * Lint fixes * Link to different types of addresses * Make content parts clickable only (a within a) * Force Chrome hardware acceleration * Trust the vendors... don't go crazy with transform :) * Use faster & default transitions * Add VaultMeta edit dialog * Updated (WIP) * Meta & password edit completed * Added SelectionList component for selections * Use SelectionList in DappPermisions * AddDapps uses SelectionList * Fix AccountCard to consistent height * Display type icons in creation dialog * Complimentary colours * Convert Signer defaults to SelectionList * Fix Geth import - actually pass addresses through * Work from addresses returned via RPC * Display actual addresses imported (not selected) * Update tests to cover bug fixed * Prettyfy Geth import * Description on selection actions * SelectionList as entry point * Update failing tests * Subtle selection border * Styling updates for account details * Add ModalBox summary * AddAddress updated * Display account vault information * Allow invalid addresses to display icons (e.g. vaults) * Display vault on edit meta * Convert VaultAccounts to SelectionList * Allow editing of Vault in meta * Add tests for SectionList component * Add tests for ModalBox component * Add tests for VaultSelector component * Add vaultsOpened in store * Add ~/ui/Form/VaultSelect * WIP * Fix failing tests * Move account to vault when selected * Fix circular build deps * EditMeta uses Form/VaultSelect * Vault move into meta store (alignment) * Re-apply stretch fix * Display vault in account summary * Add busy indicators to relevant modals * Auto-focus description field (aligns with #4657) * Remove extra container (double scrolling) * Remove unused container style * Apply scroll fixes from lates commit in #4621 * Remove unneeded logs * Remove extra div, fixing ParityBar overflow * Make dapp iframe background white * Stop event propgation on tag click * ChangeVault component (re-usable) * Use ChangeVault component * Pass vaultStores in * Icon highlight colour * Tag-ify vault name display * ChangeVault location * Bothced merge, selector rendering twice * Value can be undefined (no vault) * Close selector on Select bug * Fix toggle botched merge * Update tests * Add Vault Tags to Account Header --- .../CreateAccount/ChangeVault/changeVault.js | 51 ++++++++ .../ChangeVault/changeVault.spec.js | 100 ++++++++++++++++ .../modals/CreateAccount/ChangeVault/index.js | 17 +++ .../CreateAccount/NewAccount/newAccount.js | 8 +- .../CreateAccount/NewImport/newImport.js | 9 +- js/src/modals/CreateAccount/RawKey/rawKey.js | 8 +- .../RecoveryPhrase/recoveryPhrase.js | 8 +- js/src/modals/CreateAccount/createAccount.css | 1 + js/src/modals/CreateAccount/createAccount.js | 30 ++++- .../CreateAccount/createAccount.test.js | 4 +- js/src/modals/CreateAccount/store.js | 29 ++++- js/src/modals/CreateAccount/store.spec.js | 91 ++++++++++++--- js/src/modals/DeleteAccount/deleteAccount.js | 11 +- js/src/modals/EditMeta/editMeta.js | 62 ++-------- js/src/modals/EditMeta/store.js | 27 +++-- js/src/modals/EditMeta/store.spec.js | 72 +++++++++--- js/src/modals/VaultSelector/vaultSelector.js | 11 +- .../VaultSelector/vaultSelector.spec.js | 4 +- js/src/ui/Balance/balance.css | 2 +- js/src/ui/Certifications/certifications.css | 6 +- js/src/ui/Form/VaultSelect/index.js | 17 +++ js/src/ui/Form/VaultSelect/vaultSelect.js | 109 ++++++++++++++++++ .../ui/Form/VaultSelect/vaultSelect.spec.js | 90 +++++++++++++++ js/src/ui/Form/index.js | 1 + js/src/ui/SelectionList/selectionList.css | 1 + js/src/ui/VaultTag/index.js | 17 +++ js/src/ui/VaultTag/vaultTag.css | 48 ++++++++ js/src/ui/VaultTag/vaultTag.js | 45 ++++++++ js/src/ui/index.js | 3 +- js/src/views/Account/Header/header.css | 1 + js/src/views/Account/Header/header.js | 14 +-- js/src/views/Accounts/Summary/summary.js | 13 ++- js/src/views/Accounts/accounts.css | 8 +- js/src/views/Vaults/store.js | 2 + js/src/views/Vaults/store.spec.js | 36 ++++++ 35 files changed, 822 insertions(+), 134 deletions(-) create mode 100644 js/src/modals/CreateAccount/ChangeVault/changeVault.js create mode 100644 js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js create mode 100644 js/src/modals/CreateAccount/ChangeVault/index.js create mode 100644 js/src/ui/Form/VaultSelect/index.js create mode 100644 js/src/ui/Form/VaultSelect/vaultSelect.js create mode 100644 js/src/ui/Form/VaultSelect/vaultSelect.spec.js create mode 100644 js/src/ui/VaultTag/index.js create mode 100644 js/src/ui/VaultTag/vaultTag.css create mode 100644 js/src/ui/VaultTag/vaultTag.js diff --git a/js/src/modals/CreateAccount/ChangeVault/changeVault.js b/js/src/modals/CreateAccount/ChangeVault/changeVault.js new file mode 100644 index 000000000..566fa402c --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/changeVault.js @@ -0,0 +1,51 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import { observer } from 'mobx-react'; +import React, { Component, PropTypes } from 'react'; + +import { VaultSelect } from '~/ui'; + +@observer +export default class ChangeVault extends Component { + static propTypes = { + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object + } + + render () { + const { store, vaultStore } = this.props; + const { vaultName } = store; + + if (!vaultStore || vaultStore.vaultsOpened.length === 0) { + return null; + } + + return ( + <VaultSelect + onSelect={ this.onSelect } + value={ vaultName } + vaultStore={ vaultStore } + /> + ); + } + + onSelect = (vaultName) => { + const { store } = this.props; + + store.setVaultName(vaultName); + } +} diff --git a/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js b/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js new file mode 100644 index 000000000..a2fcb834b --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js @@ -0,0 +1,100 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import ChangeVault from './'; + +let component; +let instance; +let store; +let vaultStore; + +function createStore () { + store = { + setVaultName: sinon.stub(), + vaultName: 'testing' + }; + + return store; +} + +function createVaultStore () { + vaultStore = { + vaultsOpened: ['testing'] + }; + + return vaultStore; +} + +function render () { + component = shallow( + <ChangeVault + store={ createStore() } + vaultStore={ createVaultStore() } + /> + ); + instance = component.instance(); + + return component; +} + +describe('modals/CreateAccount/ChangeVault', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('components', () => { + describe('VaultSelect', () => { + let select; + + beforeEach(() => { + select = component.find('VaultSelect'); + }); + + it('renders', () => { + expect(select.get(0)).to.be.ok; + }); + + it('passes onSelect as instance method', () => { + expect(select.props().onSelect).to.equal(instance.onSelect); + }); + + it('passes the value', () => { + expect(select.props().value).to.equal('testing'); + }); + + it('passes the vaultStore', () => { + expect(select.props().vaultStore).to.equal(vaultStore); + }); + }); + }); + + describe('instance methods', () => { + describe('onSelect', () => { + it('calls into store setVaultName', () => { + instance.onSelect('newName'); + expect(store.setVaultName).to.have.been.calledWith('newName'); + }); + }); + }); +}); diff --git a/js/src/modals/CreateAccount/ChangeVault/index.js b/js/src/modals/CreateAccount/ChangeVault/index.js new file mode 100644 index 000000000..5eac8b21d --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +export default from './changeVault'; diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js index 3bf34b27e..e0a2f8ba2 100644 --- a/js/src/modals/CreateAccount/NewAccount/newAccount.js +++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js @@ -24,13 +24,15 @@ import { Form, Input, IdentityIcon } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; import { RefreshIcon } from '~/ui/Icons'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class CreateAccount extends Component { static propTypes = { newError: PropTypes.func.isRequired, - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } state = { @@ -123,6 +125,10 @@ export default class CreateAccount extends Component { </div> </div> <PasswordStrength input={ password } /> + <ChangeVault + store={ this.props.store } + vaultStore={ this.props.vaultStore } + /> { this.renderIdentitySelector() } { this.renderIdentities() } </Form> diff --git a/js/src/modals/CreateAccount/NewImport/newImport.js b/js/src/modals/CreateAccount/NewImport/newImport.js index 121f0be57..e3d888c3f 100644 --- a/js/src/modals/CreateAccount/NewImport/newImport.js +++ b/js/src/modals/CreateAccount/NewImport/newImport.js @@ -20,12 +20,15 @@ import { FormattedMessage } from 'react-intl'; import { Form, FileSelect, Input } from '~/ui'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class NewImport extends Component { static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object + } render () { @@ -88,6 +91,10 @@ export default class NewImport extends Component { /> </div> </div> + <ChangeVault + store={ this.props.store } + vaultStore={ this.props.vaultStore } + /> { this.renderFileSelector() } </Form> ); diff --git a/js/src/modals/CreateAccount/RawKey/rawKey.js b/js/src/modals/CreateAccount/RawKey/rawKey.js index ad96064bd..7f31cb066 100644 --- a/js/src/modals/CreateAccount/RawKey/rawKey.js +++ b/js/src/modals/CreateAccount/RawKey/rawKey.js @@ -21,6 +21,7 @@ import { FormattedMessage } from 'react-intl'; import { Form, Input } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer @@ -30,7 +31,8 @@ export default class RawKey extends Component { } static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } render () { @@ -131,6 +133,10 @@ export default class RawKey extends Component { </div> </div> <PasswordStrength input={ password } /> + <ChangeVault + store={ this.props.store } + vaultStore={ this.props.vaultStore } + /> </Form> ); } diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js index 894daa767..1e49f821f 100644 --- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js +++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js @@ -22,12 +22,14 @@ import { Checkbox } from 'material-ui'; import { Form, Input } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class RecoveryPhrase extends Component { static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } render () { @@ -127,6 +129,10 @@ export default class RecoveryPhrase extends Component { </div> </div> <PasswordStrength input={ password } /> + <ChangeVault + store={ this.props.store } + vaultStore={ this.props.vaultStore } + /> <Checkbox checked={ isWindowsPhrase } className={ styles.checkbox } diff --git a/js/src/modals/CreateAccount/createAccount.css b/js/src/modals/CreateAccount/createAccount.css index 46e327e90..1dd338643 100644 --- a/js/src/modals/CreateAccount/createAccount.css +++ b/js/src/modals/CreateAccount/createAccount.css @@ -109,6 +109,7 @@ display: flex; .icon { + color: rgb(167, 151, 0) !important; flex: 0 0 56px; height: 56px !important; margin-right: 0.75em; diff --git a/js/src/modals/CreateAccount/createAccount.js b/js/src/modals/CreateAccount/createAccount.js index c2db3d060..d89c2afa8 100644 --- a/js/src/modals/CreateAccount/createAccount.js +++ b/js/src/modals/CreateAccount/createAccount.js @@ -20,11 +20,13 @@ import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg'; import { createIdentityImg } from '~/api/util/identity'; import { newError } from '~/redux/actions'; import { Button, ModalBox, Portal } from '~/ui'; import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons'; -import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg'; + +import VaultStore from '~/views/Vaults/store'; import AccountDetails from './AccountDetails'; import AccountDetailsGeth from './AccountDetailsGeth'; @@ -82,13 +84,19 @@ class CreateAccount extends Component { } store = new Store(this.context.api, this.props.accounts); + vaultStore = VaultStore.get(this.context.api); + + componentWillMount () { + return this.vaultStore.loadVaults(); + } render () { - const { createType, stage } = this.store; + const { isBusy, createType, stage } = this.store; return ( <Portal buttons={ this.renderDialogActions() } + busy={ isBusy } activeStep={ stage } onClose={ this.onClose } open @@ -120,6 +128,7 @@ class CreateAccount extends Component { <NewAccount newError={ this.props.newError } store={ this.store } + vaultStore={ this.vaultStore } /> ); } @@ -132,18 +141,27 @@ class CreateAccount extends Component { if (createType === 'fromPhrase') { return ( - <RecoveryPhrase store={ this.store } /> + <RecoveryPhrase + store={ this.store } + vaultStore={ this.vaultStore } + /> ); } if (createType === 'fromRaw') { return ( - <RawKey store={ this.store } /> + <RawKey + store={ this.store } + vaultStore={ this.vaultStore } + /> ); } return ( - <NewImport store={ this.store } /> + <NewImport + store={ this.store } + vaultStore={ this.vaultStore } + /> ); case STAGE_INFO: @@ -266,7 +284,7 @@ class CreateAccount extends Component { this.store.setBusy(true); return this.store - .createAccount() + .createAccount(this.vaultStore) .then(() => { this.store.setBusy(false); this.store.nextStage(); diff --git a/js/src/modals/CreateAccount/createAccount.test.js b/js/src/modals/CreateAccount/createAccount.test.js index d66729aec..d4ba5a0a4 100644 --- a/js/src/modals/CreateAccount/createAccount.test.js +++ b/js/src/modals/CreateAccount/createAccount.test.js @@ -42,7 +42,9 @@ function createApi () { newAccountFromWallet: sinon.stub().resolves(ADDRESS), phraseToAddress: () => Promise.resolve(`${++counter}`), setAccountMeta: sinon.stub().resolves(), - setAccountName: sinon.stub().resolves() + setAccountName: sinon.stub().resolves(), + listVaults: sinon.stub().resolves([]), + listOpenedVaults: sinon.stub().resolves([]) } }; } diff --git a/js/src/modals/CreateAccount/store.js b/js/src/modals/CreateAccount/store.js index 7371e8df3..c76102d5b 100644 --- a/js/src/modals/CreateAccount/store.js +++ b/js/src/modals/CreateAccount/store.js @@ -44,6 +44,7 @@ export default class Store { @observable rawKey = ''; @observable rawKeyError = ERRORS.nokey; @observable stage = STAGE_SELECT_TYPE; + @observable vaultName = ''; @observable walletFile = ''; @observable walletFileError = ERRORS.noFile; @observable walletJson = ''; @@ -95,6 +96,7 @@ export default class Store { this.nameError = null; this.rawKey = ''; this.rawKeyError = null; + this.vaultName = ''; this.walletFile = ''; this.walletFileError = null; this.walletJson = ''; @@ -134,6 +136,10 @@ export default class Store { this.gethImported = gethImported; } + @action setVaultName = (vaultName) => { + this.vaultName = vaultName; + } + @action setWindowsPhrase = (isWindowsPhrase = false) => { this.isWindowsPhrase = isWindowsPhrase; } @@ -220,7 +226,28 @@ export default class Store { this.stage--; } - createAccount = () => { + createAccount = (vaultStore) => { + this.setBusy(true); + + return this + ._createAccount() + .then(() => { + if (vaultStore && this.vaultName && this.vaultName.length) { + return vaultStore.moveAccount(this.vaultName, this.address); + } + + return true; + }) + .then(() => { + this.setBusy(false); + }) + .catch((error) => { + this.setBusy(false); + throw error; + }); + } + + _createAccount = () => { switch (this.createType) { case 'fromGeth': return this.createAccountFromGeth(); diff --git a/js/src/modals/CreateAccount/store.spec.js b/js/src/modals/CreateAccount/store.spec.js index 67303fa21..833cb7ef5 100644 --- a/js/src/modals/CreateAccount/store.spec.js +++ b/js/src/modals/CreateAccount/store.spec.js @@ -22,8 +22,20 @@ import { ACCOUNTS, ADDRESS, GETH_ADDRESSES, createApi } from './createAccount.te let api; let store; +let vaultStore; + +function createVaultStore () { + vaultStore = { + moveAccount: sinon.stub().resolves(), + listVaults: sinon.stub().resolves() + }; + + return vaultStore; +} function createStore (loadGeth) { + createVaultStore(); + api = createApi(); store = new Store(api, ACCOUNTS, loadGeth); @@ -65,8 +77,9 @@ describe('modals/CreateAccount/Store', () => { describe('@action', () => { describe('clearErrors', () => { beforeEach(() => { - store.setName(''); - store.setPassword('123'); + store.setName('testing'); + store.setPassword('testing'); + store.setVaultName('testing'); store.setRawKey('test'); store.setWalletFile('test'); store.setWalletJson('test'); @@ -75,10 +88,13 @@ describe('modals/CreateAccount/Store', () => { it('clears all errors', () => { store.clearErrors(); + expect(store.name).to.equal(''); expect(store.nameError).to.be.null; + expect(store.password).to.equal(''); expect(store.passwordRepeatError).to.be.null; expect(store.rawKey).to.equal(''); expect(store.rawKeyError).to.be.null; + expect(store.vaultName).to.equal(''); expect(store.walletFile).to.equal(''); expect(store.walletFileError).to.be.null; expect(store.walletJson).to.equal(''); @@ -198,6 +214,13 @@ describe('modals/CreateAccount/Store', () => { }); }); + describe('setVaultName', () => { + it('sets the vault name', () => { + store.setVaultName('testVault'); + expect(store.vaultName).to.equal('testVault'); + }); + }); + describe('setWalletFile', () => { it('sets the filepath', () => { store.setWalletFile('testing'); @@ -384,12 +407,22 @@ describe('modals/CreateAccount/Store', () => { let createAccountFromWalletSpy; let createAccountFromPhraseSpy; let createAccountFromRawSpy; + let busySpy; beforeEach(() => { createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth'); createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet'); createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase'); createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw'); + busySpy = sinon.spy(store, 'setBusy'); + }); + + afterEach(() => { + store.createAccountFromGeth.restore(); + store.createAccountFromWallet.restore(); + store.createAccountFromPhrase.restore(); + store.createAccountFromRaw.restore(); + store.setBusy.restore(); }); it('throws error on invalid createType', () => { @@ -399,38 +432,68 @@ describe('modals/CreateAccount/Store', () => { it('calls createAccountFromGeth on createType === fromGeth', () => { store.setCreateType('fromGeth'); - store.createAccount(); - expect(createAccountFromGethSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromGethSpy).to.have.been.called; + }); }); it('calls createAccountFromWallet on createType === fromJSON', () => { store.setCreateType('fromJSON'); - store.createAccount(); - expect(createAccountFromWalletSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromWalletSpy).to.have.been.called; + }); }); it('calls createAccountFromPhrase on createType === fromNew', () => { store.setCreateType('fromNew'); - store.createAccount(); - expect(createAccountFromPhraseSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromPhraseSpy).to.have.been.called; + }); }); it('calls createAccountFromPhrase on createType === fromPhrase', () => { store.setCreateType('fromPhrase'); - store.createAccount(); - expect(createAccountFromPhraseSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromPhraseSpy).to.have.been.called; + }); }); it('calls createAccountFromWallet on createType === fromPresale', () => { store.setCreateType('fromPresale'); - store.createAccount(); - expect(createAccountFromWalletSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromWalletSpy).to.have.been.called; + }); }); it('calls createAccountFromRaw on createType === fromRaw', () => { store.setCreateType('fromRaw'); - store.createAccount(); - expect(createAccountFromRawSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromRawSpy).to.have.been.called; + }); + }); + + it('moves account to vault when vaultName set', () => { + store.setCreateType('fromNew'); + store.setVaultName('testing'); + + return store.createAccount(vaultStore).then(() => { + expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ADDRESS); + }); + }); + + it('sets and rests the busy flag', () => { + store.setCreateType('fromNew'); + + return store.createAccount().then(() => { + expect(busySpy).to.have.been.calledWith(true); + expect(busySpy).to.have.been.calledWith(false); + }); }); describe('createAccountFromGeth', () => { diff --git a/js/src/modals/DeleteAccount/deleteAccount.js b/js/src/modals/DeleteAccount/deleteAccount.js index 8f47777c5..cd69d6bd9 100644 --- a/js/src/modals/DeleteAccount/deleteAccount.js +++ b/js/src/modals/DeleteAccount/deleteAccount.js @@ -37,25 +37,27 @@ class DeleteAccount extends Component { } state = { + isBusy: false, password: '' } render () { const { account } = this.props; - const { password } = this.state; + const { isBusy, password } = this.state; return ( <ConfirmDialog + busy={ isBusy } className={ styles.body } onConfirm={ this.onDeleteConfirmed } onDeny={ this.closeDeleteDialog } + open title={ <FormattedMessage id='deleteAccount.title' defaultMessage='confirm removal' /> } - visible > <div className={ styles.hero }> <FormattedMessage @@ -117,9 +119,13 @@ class DeleteAccount extends Component { const { account, newError } = this.props; const { password } = this.state; + this.setState({ isBusy: true }); + return api.parity .killAccount(account.address, password) .then((result) => { + this.setState({ isBusy: true }); + if (result === true) { router.push('/accounts'); this.closeDeleteDialog(); @@ -128,6 +134,7 @@ class DeleteAccount extends Component { } }) .catch((error) => { + this.setState({ isBusy: false }); console.error('onDeleteConfirmed', error); newError(new Error(`Deletion failed: ${error.message}`)); }); diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 71e222ca6..5d0c91dbe 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -21,11 +21,10 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { newError } from '~/redux/actions'; -import { Button, Form, Input, InputAddress, InputChip, Portal } from '~/ui'; +import { Button, Form, Input, InputChip, Portal, VaultSelect } from '~/ui'; import { CancelIcon, SaveIcon } from '~/ui/Icons'; import VaultStore from '~/views/Vaults/store'; -import VaultSelector from '../VaultSelector'; import Store from './store'; @observer @@ -48,11 +47,12 @@ class EditMeta extends Component { } render () { - const { description, name, nameError, tags } = this.store; + const { description, isBusy, name, nameError, tags } = this.store; return ( <Portal buttons={ this.renderActions() } + busy={ isBusy } onClose={ this.onClose } open title={ @@ -62,7 +62,6 @@ class EditMeta extends Component { /> } > - { this.renderVaultSelector() } <Form> <Input autoFocus @@ -110,7 +109,7 @@ class EditMeta extends Component { onTokensChange={ this.store.setTags } tokens={ tags.slice() } /> - { this.renderVault() } + { this.renderVaultSelector() } </Form> </Portal> ); @@ -163,7 +162,7 @@ class EditMeta extends Component { ); } - renderVault () { + renderVaultSelector () { const { isAccount, vaultName } = this.store; if (!isAccount) { @@ -171,40 +170,9 @@ class EditMeta extends Component { } return ( - <InputAddress - allowCopy={ false } - allowInvalid - readOnly - hint={ - <FormattedMessage - id='editMeta.vault.hint' - defaultMessage='the vault this account is attached to' - /> - } - label={ - <FormattedMessage - id='editMeta.vault.label' - defaultMessage='associated vault' - /> - } - onClick={ this.toggleVaultSelector } - value={ vaultName } - /> - ); - } - - renderVaultSelector () { - const { isAccount, isVaultSelectorOpen, vaultName } = this.store; - - if (!isAccount || !isVaultSelectorOpen) { - return null; - } - - return ( - <VaultSelector - onClose={ this.toggleVaultSelector } + <VaultSelect onSelect={ this.setVaultName } - selected={ vaultName } + value={ vaultName } vaultStore={ this.vaultStore } /> ); @@ -215,21 +183,12 @@ class EditMeta extends Component { } onSave = () => { - const { address, isAccount, meta, vaultName } = this.store; - if (this.store.hasError) { return; } return this.store - .save() - .then(() => { - if (isAccount && (meta.vault !== vaultName)) { - return this.vaultStore.moveAccount(vaultName, address); - } - - return true; - }) + .save(this.vaultStore) .then(this.onClose) .catch((error) => { this.props.newError(error); @@ -238,11 +197,6 @@ class EditMeta extends Component { setVaultName = (vaultName) => { this.store.setVaultName(vaultName); - this.toggleVaultSelector(); - } - - toggleVaultSelector = () => { - this.store.toggleVaultSelector(); } } diff --git a/js/src/modals/EditMeta/store.js b/js/src/modals/EditMeta/store.js index 46951f095..da3d88cd7 100644 --- a/js/src/modals/EditMeta/store.js +++ b/js/src/modals/EditMeta/store.js @@ -21,7 +21,7 @@ import { validateName } from '~/util/validation'; export default class Store { @observable address = null; @observable isAccount = false; - @observable isVaultSelectorOpen = false; + @observable isBusy = false; @observable description = null; @observable meta = null; @observable name = null; @@ -73,6 +73,10 @@ export default class Store { this.passwordHint = passwordHint; } + @action setBusy = (isBusy) => { + this.isBusy = isBusy; + } + @action setTags = (tags) => { this.tags = tags.slice(); } @@ -81,11 +85,9 @@ export default class Store { this.vaultName = vaultName; } - @action setVaultSelectorOpen = (isOpen) => { - this.isVaultSelectorOpen = isOpen; - } + save (vaultStore) { + this.setBusy(true); - save () { const meta = { description: this.description, tags: this.tags.peek() @@ -100,13 +102,20 @@ export default class Store { this._api.parity.setAccountName(this.address, this.name), this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta)) ]) + .then(() => { + if (vaultStore && this.isAccount && (this.meta.vault !== this.vaultName)) { + return vaultStore.moveAccount(this.vaultName, this.address); + } + + return true; + }) + .then(() => { + this.setBusy(false); + }) .catch((error) => { console.error('onSave', error); + this.setBusy(false); throw error; }); } - - toggleVaultSelector () { - this.setVaultSelectorOpen(!this.isVaultSelectorOpen); - } } diff --git a/js/src/modals/EditMeta/store.spec.js b/js/src/modals/EditMeta/store.spec.js index 4ff775718..a38da055f 100644 --- a/js/src/modals/EditMeta/store.spec.js +++ b/js/src/modals/EditMeta/store.spec.js @@ -14,14 +14,24 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import sinon from 'sinon'; + import Store from './store'; import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js'; let api; let store; +let vaultStore; + +function createVaultStore () { + return { + moveAccount: sinon.stub().resolves(true) + }; +} function createStore (account) { api = createApi(); + vaultStore = createVaultStore(); store = new Store(api, account); @@ -108,6 +118,13 @@ describe('modals/EditMeta/Store', () => { createStore(ADDRESS); }); + describe('setBusy', () => { + it('sets the isBusy flag', () => { + store.setBusy('testing'); + expect(store.isBusy).to.equal('testing'); + }); + }); + describe('setDescription', () => { it('sets the description', () => { store.setDescription('description'); @@ -149,26 +166,56 @@ describe('modals/EditMeta/Store', () => { expect(store.vaultName).to.equal('testing'); }); }); - - describe('setVaultSelectorOpen', () => { - it('sets the state', () => { - store.setVaultSelectorOpen('testing'); - expect(store.isVaultSelectorOpen).to.equal('testing'); - }); - }); }); describe('operations', () => { describe('save', () => { beforeEach(() => { createStore(ACCOUNT); + sinon.spy(store, 'setBusy'); + }); + + afterEach(() => { + store.setBusy.restore(); + }); + + it('sets the busy flag, clearing it when done', () => { + return store.save().then(() => { + expect(store.setBusy).to.have.been.calledWith(true); + expect(store.setBusy).to.have.been.calledWith(false); + }); }); it('calls parity.setAccountName with the set value', () => { store.setName('test name'); - store.save(); - expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name'); + return store.save().then(() => { + expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name'); + }); + }); + + it('calls parity.setAccountMeta with the adjusted values', () => { + store.setDescription('some new description'); + store.setPasswordHint('some new passwordhint'); + store.setTags(['taga']); + + return store.save().then(() => { + expect(api.parity.setAccountMeta).to.have.been.calledWith( + ACCOUNT.address, Object.assign({}, ACCOUNT.meta, { + description: 'some new description', + passwordHint: 'some new passwordhint', + tags: ['taga'] + }) + ); + }); + }); + + it('moves vault account when applicable', () => { + store.setVaultName('testing'); + + return store.save(vaultStore).then(() => { + expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ACCOUNT.address); + }); }); it('calls parity.setAccountMeta with the adjusted values', () => { @@ -185,11 +232,4 @@ describe('modals/EditMeta/Store', () => { }); }); }); - - describe('toggleVaultSelector', () => { - it('inverts the selector state', () => { - store.toggleVaultSelector(); - expect(store.isVaultSelectorOpen).to.be.true; - }); - }); }); diff --git a/js/src/modals/VaultSelector/vaultSelector.js b/js/src/modals/VaultSelector/vaultSelector.js index a2ae48294..4fceb5cb0 100644 --- a/js/src/modals/VaultSelector/vaultSelector.js +++ b/js/src/modals/VaultSelector/vaultSelector.js @@ -18,7 +18,9 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Portal, SelectionList, VaultCard } from '~/ui'; +import Portal from '~/ui/Portal'; +import SelectionList from '~/ui/SelectionList'; +import VaultCard from '~/ui/VaultCard'; @observer export default class VaultSelector extends Component { @@ -48,10 +50,9 @@ export default class VaultSelector extends Component { } renderList () { - const { vaults } = this.props.vaultStore; - const openVaults = vaults.filter((vault) => vault.isOpen); + const { vaultsOpened } = this.props.vaultStore; - if (openVaults.length === 0) { + if (vaultsOpened.length === 0) { return ( <FormattedMessage id='vaults.selector.noneAvailable' @@ -62,7 +63,7 @@ export default class VaultSelector extends Component { return ( <SelectionList - items={ openVaults } + items={ vaultsOpened } isChecked={ this.isSelected } noStretch onSelectClick={ this.onSelect } diff --git a/js/src/modals/VaultSelector/vaultSelector.spec.js b/js/src/modals/VaultSelector/vaultSelector.spec.js index 2be5c4637..1996af3a7 100644 --- a/js/src/modals/VaultSelector/vaultSelector.spec.js +++ b/js/src/modals/VaultSelector/vaultSelector.spec.js @@ -28,6 +28,7 @@ const VAULTS_CLOSED = [ { name: 'C' }, { name: 'D' } ]; +const VAULTS_ALL = VAULTS_OPENED.concat(VAULTS_CLOSED); let component; let instance; @@ -37,7 +38,8 @@ let vaultStore; function createVaultStore () { vaultStore = { - vaults: VAULTS_OPENED.concat(VAULTS_CLOSED) + vaults: VAULTS_ALL, + vaultsOpened: VAULTS_OPENED }; return vaultStore; diff --git a/js/src/ui/Balance/balance.css b/js/src/ui/Balance/balance.css index 81e0fa798..1d0c9fbf3 100644 --- a/js/src/ui/Balance/balance.css +++ b/js/src/ui/Balance/balance.css @@ -18,7 +18,7 @@ .balances { display: flex; flex-wrap: wrap; - margin: 1em 0 0 0; + margin: 0.75em 0 0 0; vertical-align: top; } diff --git a/js/src/ui/Certifications/certifications.css b/js/src/ui/Certifications/certifications.css index 8e16cf2d5..4d1944b25 100644 --- a/js/src/ui/Certifications/certifications.css +++ b/js/src/ui/Certifications/certifications.css @@ -16,7 +16,7 @@ */ .certifications { - margin-top: 1em; + margin-top: 0.75em; } .certification, @@ -43,7 +43,7 @@ background-color: rgba(255, 255, 255, 0.07); margin-right: 0.5em; margin-top: 1em; - padding: 0.3em 0.6em 0.2em 2.6em; + padding: 0.3em 0.6em 0.2em 3em; text-transform: uppercase; &:last-child { @@ -52,7 +52,7 @@ .icon { position: absolute; - top: -.25em; + top: -0.25em; left: 0; } } diff --git a/js/src/ui/Form/VaultSelect/index.js b/js/src/ui/Form/VaultSelect/index.js new file mode 100644 index 000000000..b8741956e --- /dev/null +++ b/js/src/ui/Form/VaultSelect/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +export default from './vaultSelect'; diff --git a/js/src/ui/Form/VaultSelect/vaultSelect.js b/js/src/ui/Form/VaultSelect/vaultSelect.js new file mode 100644 index 000000000..c93e530e1 --- /dev/null +++ b/js/src/ui/Form/VaultSelect/vaultSelect.js @@ -0,0 +1,109 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import VaultSelector from '~/modals/VaultSelector'; +import VaultStore from '~/views/Vaults/store'; + +import InputAddress from '../InputAddress'; + +export default class VaultSelect extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + }; + + static propTypes = { + onSelect: PropTypes.func.isRequired, + value: PropTypes.string, + vaultStore: PropTypes.object + }; + + state = { + isOpen: false + }; + + vaultStore = this.props.vaultStore || VaultStore.get(this.context.api); + + componentWillMount () { + return this.vaultStore.loadVaults(); + } + + render () { + const { value } = this.props; + + return ( + <div> + { this.renderSelector() } + <InputAddress + allowCopy={ false } + allowInvalid + disabled + hint={ + <FormattedMessage + id='ui.vaultSelect.hint' + defaultMessage='the vault this account is attached to' + /> + } + label={ + <FormattedMessage + id='ui.vaultSelect.label' + defaultMessage='associated vault' + /> + } + onClick={ this.openSelector } + value={ (value || '').toUpperCase() } + /> + </div> + ); + } + + renderSelector () { + const { value } = this.props; + const { isOpen } = this.state; + + if (!isOpen) { + return null; + } + + return ( + <VaultSelector + onClose={ this.closeSelector } + onSelect={ this.onSelect } + selected={ value } + vaultStore={ this.vaultStore } + /> + ); + } + + openSelector = () => { + this.setState({ + isOpen: true + }); + } + + closeSelector = () => { + this.setState({ + isOpen: false + }); + } + + onSelect = (vaultName) => { + this.props.onSelect(vaultName); + this.closeSelector(); + } +} diff --git a/js/src/ui/Form/VaultSelect/vaultSelect.spec.js b/js/src/ui/Form/VaultSelect/vaultSelect.spec.js new file mode 100644 index 000000000..a0d5ed583 --- /dev/null +++ b/js/src/ui/Form/VaultSelect/vaultSelect.spec.js @@ -0,0 +1,90 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import VaultSelect from './'; + +let component; +let instance; +let onSelect; +let vaultStore; + +function createVaultStore () { + vaultStore = { + loadVaults: sinon.stub().resolves(true) + }; + + return vaultStore; +} + +function render () { + onSelect = sinon.stub(); + + component = shallow( + <VaultSelect + onSelect={ onSelect } + value='initialValue' + vaultStore={ createVaultStore() } + /> + ); + instance = component.instance(); + + return component; +} + +describe('ui/Form/VaultSelect', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('components', () => { + describe('InputAddress', () => { + let input; + + beforeEach(() => { + input = component.find('Connect(InputAddress)'); + }); + + it('renders', () => { + expect(input.get(0)).to.be.ok; + }); + + it('passes value from props', () => { + expect(input.props().value).to.equal('INITIALVALUE'); + }); + + it('passes instance openSelector to onClick', () => { + expect(input.props().onClick).to.equal(instance.openSelector); + }); + }); + }); + + describe('instance methods', () => { + describe('onSelect', () => { + it('calls into props', () => { + instance.onSelect('testing'); + expect(onSelect).to.have.been.calledWith('testing'); + }); + }); + }); +}); diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index bb5516c89..8c8c7e1f2 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -29,5 +29,6 @@ export Label from './Label'; export RadioButtons from './RadioButtons'; export Select from './Select'; export TypedInput from './TypedInput'; +export VaultSelect from './VaultSelect'; export default from './form'; diff --git a/js/src/ui/SelectionList/selectionList.css b/js/src/ui/SelectionList/selectionList.css index 44f5a39b3..6a1a37eaf 100644 --- a/js/src/ui/SelectionList/selectionList.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -17,6 +17,7 @@ .item { border: 2px solid transparent; + cursor: pointer; display: flex; flex: 1; height: 100%; diff --git a/js/src/ui/VaultTag/index.js b/js/src/ui/VaultTag/index.js new file mode 100644 index 000000000..af0419c99 --- /dev/null +++ b/js/src/ui/VaultTag/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +export default from './vaultTag'; diff --git a/js/src/ui/VaultTag/vaultTag.css b/js/src/ui/VaultTag/vaultTag.css new file mode 100644 index 000000000..acb139c6e --- /dev/null +++ b/js/src/ui/VaultTag/vaultTag.css @@ -0,0 +1,48 @@ +/* Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. +*/ + +/* TODO: These tag styles are shared with Balances & Certifications - should be made into +/* a component that can take a list of tags and render them in the correct format +*/ +.vault { + display: flex; + flex-wrap: wrap; + margin: 0.75em 0 0; + vertical-align: top; + + .vaultBody { + margin: 0.75em 0.5em 0 0; + background: rgba(255, 255, 255, 0.07); + border-radius: 16px; + max-height: 24px; + max-width: 100%; + display: flex; + align-items: center; + } + + img { + height: 32px !important; + margin: -4px 1em 0 0; + width: 32px !important; + } + + .text { + margin: 0 0.5em 0 0; + text-transform: uppercase; + white-space: nowrap; + } +} diff --git a/js/src/ui/VaultTag/vaultTag.js b/js/src/ui/VaultTag/vaultTag.js new file mode 100644 index 000000000..303aaca61 --- /dev/null +++ b/js/src/ui/VaultTag/vaultTag.js @@ -0,0 +1,45 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import React, { Component, PropTypes } from 'react'; + +import IdentityIcon from '~/ui/IdentityIcon'; + +import styles from './vaultTag.css'; + +export default class VaultTag extends Component { + static propTypes = { + vault: PropTypes.string.isRequired + }; + + render () { + const { vault } = this.props; + + return ( + <div className={ styles.vault }> + <div className={ styles.vaultBody }> + <IdentityIcon + address={ vault } + inline + /> + <div className={ styles.text }> + { vault } + </div> + </div> + </div> + ); + } +} diff --git a/js/src/ui/index.js b/js/src/ui/index.js index ae1ce8451..d076986be 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -30,7 +30,7 @@ export DappCard from './DappCard'; export DappIcon from './DappIcon'; export Errors from './Errors'; export Features, { FEATURES, FeaturesStore } from './Features'; -export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; +export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput, VaultSelect } from './Form'; export GasPriceEditor from './GasPriceEditor'; export GasPriceSelector from './GasPriceSelector'; export Icons from './Icons'; @@ -56,4 +56,5 @@ export Tooltips, { Tooltip } from './Tooltips'; export TxHash from './TxHash'; export TxList from './TxList'; export VaultCard from './VaultCard'; +export VaultTag from './VaultTag'; export Warning from './Warning'; diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 62f072574..f894b7c49 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -66,6 +66,7 @@ .text { display: inline-block; opacity: 0.25; + text-transform: uppercase; } } diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index dc367d136..d3c4a9c69 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -17,7 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags } from '~/ui'; +import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags, VaultTag } from '~/ui'; import styles from './header.css'; @@ -69,7 +69,6 @@ export default class Header extends Component { { address } </div> </div> - { this.renderVault() } { this.renderUuid() } <div className={ styles.infoline }> { meta.description } @@ -81,6 +80,7 @@ export default class Header extends Component { balance={ balance } /> <Certifications address={ address } /> + { this.renderVault() } </div> </div> <div className={ styles.tags }> @@ -169,15 +169,7 @@ export default class Header extends Component { } return ( - <div className={ styles.vault }> - <IdentityIcon - address={ meta.vault } - inline - /> - <div className={ styles.text }> - { meta.vault } - </div> - </div> + <VaultTag vault={ meta.vault } /> ); } } diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 3a4b282fa..78f4dd5c4 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -22,7 +22,7 @@ import { isEqual } from 'lodash'; import ReactTooltip from 'react-tooltip'; import { FormattedMessage } from 'react-intl'; -import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags } from '~/ui'; +import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags, VaultTag } from '~/ui'; import Certifications from '~/ui/Certifications'; import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; @@ -117,6 +117,7 @@ class Summary extends Component { { this.renderDescription(account.meta) } { this.renderOwners() } { this.renderCertifications() } + { this.renderVault(account.meta) } </div> } link={ this.getLink() } @@ -287,6 +288,16 @@ class Summary extends Component { /> ); } + + renderVault (meta) { + if (!meta || !meta.vault) { + return null; + } + + return ( + <VaultTag vault={ meta.vault } /> + ); + } } function mapStateToProps (state) { diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index de6ce3ab5..296ae6714 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -56,6 +56,10 @@ } } + .overlay { + margin-top: -3.25em; + } + .owners { display: flex; justify-content: center; @@ -68,10 +72,6 @@ } } - .overlay { - margin-top: -3.25em; - } - &:not(:hover) { .tags { display: none; diff --git a/js/src/views/Vaults/store.js b/js/src/views/Vaults/store.js index 75a52954d..2d4f4c2df 100644 --- a/js/src/views/Vaults/store.js +++ b/js/src/views/Vaults/store.js @@ -37,6 +37,7 @@ export default class Store { @observable selectedAccounts = {}; @observable vault = null; @observable vaults = []; + @observable vaultsOpened = []; @observable vaultNames = []; @observable vaultName = ''; @observable vaultNameError = ERRORS.noName; @@ -143,6 +144,7 @@ export default class Store { isOpen: openedVaults.includes(name) }; }); + this.vaultsOpened = this.vaults.filter((vault) => vault.isOpen); }); } diff --git a/js/src/views/Vaults/store.spec.js b/js/src/views/Vaults/store.spec.js index 9f971d383..863b853da 100644 --- a/js/src/views/Vaults/store.spec.js +++ b/js/src/views/Vaults/store.spec.js @@ -180,6 +180,12 @@ describe('modals/Vaults/Store', () => { { name: 'some', meta: 'metaSome', isOpen: false } ]); }); + + it('sets the opened vaults', () => { + expect(store.vaultsOpened.peek()).to.deep.equal([ + { name: 'TEST', meta: 'metaTest', isOpen: true } + ]); + }); }); describe('setVaultDescription', () => { @@ -553,6 +559,36 @@ describe('modals/Vaults/Store', () => { }); }); + describe('editVaultMeta', () => { + beforeEach(() => { + sinon.spy(store, 'setBusyMeta'); + + store.setVaultDescription('testDescription'); + store.setVaultName('testCreateName'); + store.setVaultPasswordHint('testCreateHint'); + store.setVaultTags('testTags'); + + return store.editVaultMeta(); + }); + + afterEach(() => { + store.setBusyMeta.restore(); + }); + + it('sets and resets the busy flag', () => { + expect(store.setBusyMeta).to.have.been.calledWith(true); + expect(store.isBusyMeta).to.be.false; + }); + + it('calls into parity_setVaultMeta', () => { + expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', { + description: 'testDescription', + passwordHint: 'testCreateHint', + tags: 'testTags' + }); + }); + }); + describe('editVaultPassword', () => { beforeEach(() => { sinon.spy(store, 'setBusyMeta'); From edecd951bae91069a249aa968bb05912fdb44e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Fri, 3 Mar 2017 19:52:08 +0100 Subject: [PATCH 54/93] Revert last hyper "fix" (#4752) --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 605ffa8a6..9acc4cd46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -976,7 +976,7 @@ dependencies = [ [[package]] name = "hyper" version = "0.10.0-a.0" -source = "git+https://github.com/ethcore/hyper#2e6702984f4f9e99fe251537a755aff0badc0b3a" +source = "git+https://github.com/ethcore/hyper#453c683b52208fefc32d29e4ac7c863439b2321f" dependencies = [ "cookie 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", From 2e0d0054873a29f667d2057fc7633bf27953cd1a Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Fri, 3 Mar 2017 19:10:54 +0000 Subject: [PATCH 55/93] [ci skip] js-precompiled 20170303-190450 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9acc4cd46..09e108044 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#17f9c870020a896bf6a52e00be5cdf6ebd912b1b" +source = "git+https://github.com/ethcore/js-precompiled.git#2bbbab73349fbb7eb84d7314727046a812a3e3db" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 0926363c6..3f496589b 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.115", + "version": "0.3.116", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From d68ea777a285913b85a390fb7e5d9779e12f1b13 Mon Sep 17 00:00:00 2001 From: keorn <pczaban@gmail.com> Date: Fri, 3 Mar 2017 22:32:22 +0100 Subject: [PATCH 56/93] Extend authority round consensus test (#4756) * add auth round test case * correct fork assertion --- sync/src/tests/consensus.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/sync/src/tests/consensus.rs b/sync/src/tests/consensus.rs index 228096f28..2c45bbd28 100644 --- a/sync/src/tests/consensus.rs +++ b/sync/src/tests/consensus.rs @@ -83,17 +83,19 @@ fn authority_round() { net.peer(0).chain.miner().import_own_transaction(&*net.peer(0).chain, new_tx(s0.secret(), 1.into())).unwrap(); net.peer(1).chain.miner().import_own_transaction(&*net.peer(1).chain, new_tx(s1.secret(), 1.into())).unwrap(); - // Move to next proposer step + // Move to next proposer step. net.peer(0).chain.engine().step(); net.peer(1).chain.engine().step(); net.sync(); assert_eq!(net.peer(0).chain.chain_info().best_block_number, 2); assert_eq!(net.peer(1).chain.chain_info().best_block_number, 2); - // Fork the network + // Fork the network with equal height. net.peer(0).chain.miner().import_own_transaction(&*net.peer(0).chain, new_tx(s0.secret(), 2.into())).unwrap(); net.peer(1).chain.miner().import_own_transaction(&*net.peer(1).chain, new_tx(s1.secret(), 2.into())).unwrap(); + // Let both nodes build one block. net.peer(0).chain.engine().step(); + let early_hash = net.peer(0).chain.chain_info().best_block_hash; net.peer(1).chain.engine().step(); net.peer(0).chain.engine().step(); net.peer(1).chain.engine().step(); @@ -102,13 +104,39 @@ fn authority_round() { assert_eq!(ci0.best_block_number, 3); assert_eq!(ci1.best_block_number, 3); assert!(ci0.best_block_hash != ci1.best_block_hash); - // Reorg to the correct one. + // Reorg to the chain with earlier view. net.sync(); let ci0 = net.peer(0).chain.chain_info(); let ci1 = net.peer(1).chain.chain_info(); assert_eq!(ci0.best_block_number, 3); assert_eq!(ci1.best_block_number, 3); assert_eq!(ci0.best_block_hash, ci1.best_block_hash); + assert_eq!(ci1.best_block_hash, early_hash); + + // Selfish miner + net.peer(0).chain.miner().import_own_transaction(&*net.peer(0).chain, new_tx(s0.secret(), 3.into())).unwrap(); + net.peer(1).chain.miner().import_own_transaction(&*net.peer(1).chain, new_tx(s1.secret(), 3.into())).unwrap(); + // Node 0 is an earlier primary. + net.peer(0).chain.engine().step(); + assert_eq!(net.peer(0).chain.chain_info().best_block_number, 4); + net.peer(0).chain.engine().step(); + net.peer(0).chain.engine().step(); + net.peer(0).chain.engine().step(); + assert_eq!(net.peer(0).chain.chain_info().best_block_number, 4); + // Node 1 makes 2 blocks, but is a later primary on the first one. + net.peer(1).chain.engine().step(); + net.peer(1).chain.engine().step(); + net.peer(1).chain.miner().import_own_transaction(&*net.peer(1).chain, new_tx(s1.secret(), 4.into())).unwrap(); + net.peer(1).chain.engine().step(); + net.peer(1).chain.engine().step(); + assert_eq!(net.peer(1).chain.chain_info().best_block_number, 5); + // Reorg to the longest chain one not ealier view one. + net.sync(); + let ci0 = net.peer(0).chain.chain_info(); + let ci1 = net.peer(1).chain.chain_info(); + assert_eq!(ci0.best_block_number, 5); + assert_eq!(ci1.best_block_number, 5); + assert_eq!(ci0.best_block_hash, ci1.best_block_hash); } #[test] From 5a7cbb3e5b1daf334e70874783a1eee76adac374 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Sun, 5 Mar 2017 10:27:18 +0100 Subject: [PATCH 57/93] Fix invalid props (#4766) --- js/src/modals/Verification/store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/modals/Verification/store.js b/js/src/modals/Verification/store.js index b652131d9..e7986f6ae 100644 --- a/js/src/modals/Verification/store.js +++ b/js/src/modals/Verification/store.js @@ -46,7 +46,7 @@ export default class VerificationStore { @observable consentGiven = false; @observable requestTx = null; @observable code = ''; - @observable isCodeValid = null; + @observable isCodeValid = false; @observable confirmationTx = null; constructor (api, abi, certifierName, account, isTestnet) { @@ -144,7 +144,7 @@ export default class VerificationStore { const values = [ sha3.text(code) ]; this.code = code; - this.isCodeValid = null; + this.isCodeValid = false; confirm.estimateGas(options, values) .then((gas) => { options.gas = gas.mul(1.2).toFixed(0); From cabf251280938f0bd2bed682b3b33ae799229aea Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Sun, 5 Mar 2017 09:41:41 +0000 Subject: [PATCH 58/93] [ci skip] js-precompiled 20170305-093606 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09e108044..0c578977b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#2bbbab73349fbb7eb84d7314727046a812a3e3db" +source = "git+https://github.com/ethcore/js-precompiled.git#377749694cfebdea96cca5ed7ff777eef1424b66" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 3f496589b..dab289769 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.116", + "version": "0.3.117", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 944dcdc01006a57e04fb9862e08f2a8d13f3aeac Mon Sep 17 00:00:00 2001 From: Jerome de Tychey <jdetychey@gmail.com> Date: Mon, 6 Mar 2017 08:49:52 +0100 Subject: [PATCH 59/93] Update README.md (#4762) Fix for cargo fail when building from source --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 260566c5e..2511e7a4f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,11 @@ $ cargo build --release ``` This will produce an executable in the `./target/release` subdirectory. +Note: if cargo fails to parse manifest try: +```bash +$ ~/.cargo/bin/cargo build --release +``` ---- ## Simple one-line installer for Mac and Ubuntu From 0b24a3d7f64a0ee927a5d813b3a3428d61edff92 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Mon, 6 Mar 2017 08:54:59 +0100 Subject: [PATCH 60/93] Etherscan links based on netVersion identifier (#4772) * Use netVersion to determine external links * Update additional isTest references --- js/src/3rdparty/etherscan/account.js | 22 ++++++------- js/src/3rdparty/etherscan/call.js | 24 ++++++++++++-- js/src/3rdparty/etherscan/helpers.spec.js | 4 +-- js/src/3rdparty/etherscan/links.js | 33 +++++++++++++++---- js/src/dapps/registry/actions.js | 8 ++--- js/src/dapps/registry/reducers.js | 8 ++--- js/src/dapps/registry/ui/address.js | 10 +++--- js/src/dapps/registry/ui/hash.js | 8 ++--- js/src/dapps/registry/util/etherscan-url.js | 6 ++-- js/src/dapps/tokendeploy/services.js | 14 ++++---- .../modals/ExecuteContract/executeContract.js | 1 - js/src/redux/providers/status.js | 1 + js/src/redux/providers/statusReducer.js | 1 + js/src/ui/TxHash/txHash.js | 12 ++++--- js/src/ui/TxHash/txHash.spec.js | 4 ++- js/src/ui/TxList/TxRow/txRow.js | 16 +++++---- js/src/ui/TxList/TxRow/txRow.spec.js | 9 +++-- js/src/ui/TxList/txList.js | 10 +++--- js/src/ui/TxList/txList.spec.js | 2 +- js/src/views/Account/Transactions/store.js | 12 +++---- .../views/Account/Transactions/store.spec.js | 18 +++++----- .../Account/Transactions/transactions.js | 8 ++--- js/src/views/Account/account.test.js | 2 +- js/src/views/Application/TabBar/tabBar.js | 2 -- js/src/views/Application/application.js | 14 ++------ js/src/views/Contract/Events/Event/event.js | 6 ++-- js/src/views/Contract/Events/events.js | 12 ++++--- js/src/views/Contract/contract.js | 12 +++---- .../Account/AccountLink/accountLink.js | 13 ++++---- .../Signer/components/Account/account.js | 12 +++---- .../RequestPending/requestPending.js | 8 ++--- .../RequestPending/requestPending.spec.js | 2 +- .../components/SignRequest/signRequest.js | 6 ++-- .../transactionMainDetails.js | 6 ++-- .../TransactionPending/transactionPending.js | 6 ++-- .../components/TxHashLink/txHashLink.js | 6 ++-- .../Signer/containers/Embedded/embedded.js | 10 +++--- .../containers/RequestsPage/requestsPage.js | 10 +++--- .../Wallet/Confirmations/confirmations.js | 8 ++--- .../views/Wallet/Transactions/transactions.js | 6 ++-- js/src/views/Wallet/wallet.js | 20 +++++------ 41 files changed, 217 insertions(+), 175 deletions(-) diff --git a/js/src/3rdparty/etherscan/account.js b/js/src/3rdparty/etherscan/account.js index 8a8f4b1fc..7a6844759 100644 --- a/js/src/3rdparty/etherscan/account.js +++ b/js/src/3rdparty/etherscan/account.js @@ -21,15 +21,15 @@ const PAGE_SIZE = 25; import util from '../../api/util'; import { call } from './call'; -function _call (method, params, test) { - return call('account', method, params, test); +function _call (method, params, test, netVersion) { + return call('account', method, params, test, netVersion); } -function balance (address, test = false) { +function balance (address, test, netVersion) { return _call('balance', { address: address, tag: 'latest' - }, test).then((balance) => { + }, test, netVersion).then((balance) => { // same format as balancemulti below return { account: address, @@ -38,21 +38,21 @@ function balance (address, test = false) { }); } -function balances (addresses, test = false) { +function balances (addresses, test, netVersion) { return _call('balancemulti', { address: addresses.join(','), tag: 'latest' - }, test); + }, test, netVersion); } -function transactions (address, page, test = false) { +function transactions (address, page, test, netVersion) { // page offset from 0 return _call('txlist', { address: address, offset: PAGE_SIZE, page: (page || 0) + 1, sort: 'desc' - }, test).then((transactions) => { + }, test, netVersion).then((transactions) => { return transactions.map((tx) => { return { blockNumber: new BigNumber(tx.blockNumber || 0), @@ -67,9 +67,9 @@ function transactions (address, page, test = false) { } const account = { - balance: balance, - balances: balances, - transactions: transactions + balance, + balances, + transactions }; export { account }; diff --git a/js/src/3rdparty/etherscan/call.js b/js/src/3rdparty/etherscan/call.js index 3c3d1ef06..6b72e1bea 100644 --- a/js/src/3rdparty/etherscan/call.js +++ b/js/src/3rdparty/etherscan/call.js @@ -23,14 +23,32 @@ const options = { } }; -export function call (module, action, _params, test) { - const host = test ? 'testnet.etherscan.io' : 'api.etherscan.io'; +export function call (module, action, _params, test, netVersion) { + let prefix = 'api.'; + + switch (netVersion) { + case '2': + case '3': + prefix = 'testnet.'; + break; + + case '42': + prefix = 'kovan.'; + break; + + case '0': + default: + if (test) { + prefix = 'testnet.'; + } + break; + } const query = stringify(Object.assign({ module, action }, _params || {})); - return fetch(`https://${host}/api?${query}`, options) + return fetch(`https://${prefix}etherscan.io/api?${query}`, options) .then((response) => { if (!response.ok) { throw { code: response.status, message: response.statusText }; // eslint-disable-line diff --git a/js/src/3rdparty/etherscan/helpers.spec.js b/js/src/3rdparty/etherscan/helpers.spec.js index aeb6ef230..fa29c3d97 100644 --- a/js/src/3rdparty/etherscan/helpers.spec.js +++ b/js/src/3rdparty/etherscan/helpers.spec.js @@ -19,8 +19,8 @@ import { stringify } from 'qs'; import { url } from './links'; -function mockget (requests, test) { - let scope = nock(url(test)); +function mockget (requests, test, netVersion) { + let scope = nock(url(test, netVersion)); requests.forEach((request) => { scope = scope diff --git a/js/src/3rdparty/etherscan/links.js b/js/src/3rdparty/etherscan/links.js index 59ad51de7..8c9101268 100644 --- a/js/src/3rdparty/etherscan/links.js +++ b/js/src/3rdparty/etherscan/links.js @@ -14,14 +14,35 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -export const url = (isTestnet = false) => { - return `https://${isTestnet ? 'testnet.' : ''}etherscan.io`; +// NOTE: Keep 'isTestnet' for backwards library compatibility +export const url = (isTestnet = false, netVersion = '0') => { + let prefix = ''; + + switch (netVersion) { + case '2': + case '3': + prefix = 'testnet.'; + break; + + case '42': + prefix = 'kovan.'; + break; + + case '0': + default: + if (isTestnet) { + prefix = 'testnet.'; + } + break; + } + + return `https://${prefix}etherscan.io`; }; -export const txLink = (hash, isTestnet = false) => { - return `${url(isTestnet)}/tx/${hash}`; +export const txLink = (hash, isTestnet = false, netVersion = '0') => { + return `${url(isTestnet, netVersion)}/tx/${hash}`; }; -export const addressLink = (address, isTestnet = false) => { - return `${url(isTestnet)}/address/${address}`; +export const addressLink = (address, isTestnet = false, netVersion = '0') => { + return `${url(isTestnet, netVersion)}/address/${address}`; }; diff --git a/js/src/dapps/registry/actions.js b/js/src/dapps/registry/actions.js index 2346cf323..00d851a37 100644 --- a/js/src/dapps/registry/actions.js +++ b/js/src/dapps/registry/actions.js @@ -32,16 +32,12 @@ const REGISTRY_V1_HASHES = [ '0x64c3ee34851517a9faecd995c102b339f03e564ad6772dc43a26f993238b20ec' // homestead ]; -export const setIsTestnet = (isTestnet) => ({ type: 'set isTestnet', isTestnet }); +export const setNetVersion = (netVersion) => ({ type: 'set netVersion', netVersion }); export const fetchIsTestnet = () => (dispatch) => api.net.version() .then((netVersion) => { - dispatch(setIsTestnet([ - '2', // morden - '3', // ropsten - '42' // kovan - ].includes(netVersion))); + dispatch(setNetVersion(netVersion)); }) .catch((err) => { console.error('could not check if testnet'); diff --git a/js/src/dapps/registry/reducers.js b/js/src/dapps/registry/reducers.js index cf5c82f02..6d0816273 100644 --- a/js/src/dapps/registry/reducers.js +++ b/js/src/dapps/registry/reducers.js @@ -22,8 +22,8 @@ import namesReducer from './Names/reducers.js'; import recordsReducer from './Records/reducers.js'; import reverseReducer from './Reverse/reducers.js'; -const isTestnetReducer = (state = null, action) => - action.type === 'set isTestnet' ? action.isTestnet : state; +const netVersionReducer = (state = null, action) => + action.type === 'set netVersion' ? action.netVersion : state; const contractReducer = (state = null, action) => action.type === 'set contract' ? action.contract : state; @@ -35,7 +35,7 @@ const ownerReducer = (state = null, action) => action.type === 'set owner' ? action.owner : state; const initialState = { - isTestnet: isTestnetReducer(undefined, { type: '' }), + netVersion: netVersionReducer(undefined, { type: '' }), accounts: accountsReducer(undefined, { type: '' }), contacts: contactsReducer(undefined, { type: '' }), contract: contractReducer(undefined, { type: '' }), @@ -49,7 +49,7 @@ const initialState = { }; export default (state = initialState, action) => ({ - isTestnet: isTestnetReducer(state.isTestnet, action), + netVersion: netVersionReducer(state.netVersion, action), accounts: accountsReducer(state.accounts, action), contacts: contactsReducer(state.contacts, action), contract: contractReducer(state.contract, action), diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js index ab0500532..a01811fc4 100644 --- a/js/src/dapps/registry/ui/address.js +++ b/js/src/dapps/registry/ui/address.js @@ -28,7 +28,7 @@ class Address extends Component { static propTypes = { address: PropTypes.string.isRequired, account: nullableProptype(PropTypes.object.isRequired), - isTestnet: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, key: PropTypes.string, shortenHash: PropTypes.bool }; @@ -56,7 +56,7 @@ class Address extends Component { } renderCaption () { - const { address, account, isTestnet, shortenHash } = this.props; + const { address, account, netVersion, shortenHash } = this.props; if (account) { const { name } = account; @@ -64,7 +64,7 @@ class Address extends Component { return ( <a className={ styles.link } - href={ etherscanUrl(address, isTestnet) } + href={ etherscanUrl(address, false, netVersion) } target='_blank' > <abbr @@ -103,14 +103,14 @@ function mapStateToProps (initState, initProps) { }); return (state, props) => { - const { isTestnet } = state; + const { netVersion } = state; const { address = '' } = props; const account = allAccounts[address] || null; return { account, - isTestnet + netVersion }; }; } diff --git a/js/src/dapps/registry/ui/hash.js b/js/src/dapps/registry/ui/hash.js index 88099fcf0..fe404f5b2 100644 --- a/js/src/dapps/registry/ui/hash.js +++ b/js/src/dapps/registry/ui/hash.js @@ -26,7 +26,7 @@ const leading0x = /^0x/; class Hash extends Component { static propTypes = { hash: PropTypes.string.isRequired, - isTestnet: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, linked: PropTypes.bool } @@ -35,7 +35,7 @@ class Hash extends Component { } render () { - const { hash, isTestnet, linked } = this.props; + const { hash, netVersion, linked } = this.props; let shortened = hash.toLowerCase().replace(leading0x, ''); @@ -47,7 +47,7 @@ class Hash extends Component { return ( <a className={ styles.link } - href={ etherscanUrl(hash, isTestnet) } + href={ etherscanUrl(hash, false, netVersion) } target='_blank' > <abbr title={ hash }>{ shortened }</abbr> @@ -61,7 +61,7 @@ class Hash extends Component { export default connect( (state) => ({ // mapStateToProps - isTestnet: state.isTestnet + netVersion: state.netVersion }), null // mapDispatchToProps )(Hash); diff --git a/js/src/dapps/registry/util/etherscan-url.js b/js/src/dapps/registry/util/etherscan-url.js index 88094a911..bb4e2fe98 100644 --- a/js/src/dapps/registry/util/etherscan-url.js +++ b/js/src/dapps/registry/util/etherscan-url.js @@ -14,13 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { url as externalUrl } from '~/3rdparty/etherscan/links'; + const leading0x = /^0x/; -const etherscanUrl = (hash, isTestnet) => { +const etherscanUrl = (hash, isTestnet, netVersion) => { hash = hash.toLowerCase().replace(leading0x, ''); const type = hash.length === 40 ? 'address' : 'tx'; - return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/${type}/0x${hash}`; + return `https://${externalUrl(isTestnet, netVersion)}/${type}/0x${hash}`; }; export default etherscanUrl; diff --git a/js/src/dapps/tokendeploy/services.js b/js/src/dapps/tokendeploy/services.js index 1285371aa..9ca4c4f56 100644 --- a/js/src/dapps/tokendeploy/services.js +++ b/js/src/dapps/tokendeploy/services.js @@ -16,6 +16,7 @@ import BigNumber from 'bignumber.js'; +import { url as etherscanUrl } from '~/3rdparty/etherscan/links'; import * as abis from '~/contracts/abi'; import { api } from './parity'; @@ -28,7 +29,7 @@ const subscriptions = {}; let defaultSubscriptionId; let nextSubscriptionId = 1000; -let isTest = false; +let netVersion = '0'; export function subscribeEvents (addresses, callback) { const subscriptionId = nextSubscriptionId++; @@ -117,15 +118,16 @@ export function attachInstances () { return Promise .all([ api.parity.registryAddress(), - api.parity.netChain() + api.parity.netChain(), + api.partiy.netVersion() ]) - .then(([registryAddress, netChain]) => { + .then(([registryAddress, netChain, _netVersion]) => { const registry = api.newContract(abis.registry, registryAddress).instance; - isTest = ['kovan', 'morden', 'ropsten', 'testnet'].includes(netChain); + netVersion = _netVersion; console.log(`contract was found at registry=${registryAddress}`); - console.log(`running on ${netChain}, isTest=${isTest}`); + console.log(`running on ${netChain}, network ${netVersion}`); return Promise .all([ @@ -287,5 +289,5 @@ export function loadTokenBalance (tokenAddress, address) { } export function txLink (txHash) { - return `https://${isTest ? 'testnet.' : ''}etherscan.io/tx/${txHash}`; + return `https://${etherscanUrl(false, netVersion)}/tx/${txHash}`; } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 312f8bb27..20237acf9 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -84,7 +84,6 @@ class ExecuteContract extends Component { contract: PropTypes.object.isRequired, fromAddress: PropTypes.string, gasLimit: PropTypes.object.isRequired, - isTest: PropTypes.bool, onClose: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired } diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 4cf8ea186..f671fbabe 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -302,6 +302,7 @@ export default class Status { defaultExtraData, netChain, netPort, + netVersion, rpcSettings, isTest, enode diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index c8eb5b7f6..1189f51c6 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -40,6 +40,7 @@ const initialState = { max: new BigNumber(0) }, netPort: new BigNumber(0), + netVersion: '0', rpcSettings: {}, syncing: true, isConnected: false, diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index 724004ae7..c5342ccd3 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -34,8 +34,8 @@ class TxHash extends Component { static propTypes = { hash: PropTypes.string.isRequired, - isTest: PropTypes.bool, maxConfirmations: PropTypes.number, + netVersion: PropTypes.string.isRequired, summary: PropTypes.bool } @@ -116,10 +116,10 @@ class TxHash extends Component { } render () { - const { hash, isTest, summary } = this.props; + const { hash, netVersion, summary } = this.props; const hashLink = ( - <a href={ txLink(hash, isTest) } target='_blank'> + <a href={ txLink(hash, false, netVersion) } target='_blank'> <ShortenedHash data={ hash } /> </a> ); @@ -255,9 +255,11 @@ class TxHash extends Component { } function mapStateToProps (state) { - const { isTest } = state.nodeStatus; + const { netVersion } = state.nodeStatus; - return { isTest }; + return { + netVersion + }; } export default connect( diff --git a/js/src/ui/TxHash/txHash.spec.js b/js/src/ui/TxHash/txHash.spec.js index 328a43836..8e44c7e57 100644 --- a/js/src/ui/TxHash/txHash.spec.js +++ b/js/src/ui/TxHash/txHash.spec.js @@ -69,7 +69,9 @@ function createRedux () { subscribe: sinon.stub(), getState: () => { return { - nodeStatus: { isTest: true } + nodeStatus: { + netVersion: '42' + } }; } }; diff --git a/js/src/ui/TxList/TxRow/txRow.js b/js/src/ui/TxList/TxRow/txRow.js index e42f64159..ef30fc8d1 100644 --- a/js/src/ui/TxList/TxRow/txRow.js +++ b/js/src/ui/TxList/TxRow/txRow.js @@ -35,7 +35,7 @@ class TxRow extends Component { static propTypes = { accountAddresses: PropTypes.array.isRequired, address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, tx: PropTypes.object.isRequired, block: PropTypes.object, @@ -48,7 +48,7 @@ class TxRow extends Component { }; render () { - const { tx, address, isTest, historic, className } = this.props; + const { address, className, historic, netVersion, tx } = this.props; return ( <tr className={ className || '' }> @@ -60,7 +60,7 @@ class TxRow extends Component { <div> <a className={ styles.link } - href={ txLink(tx.hash, isTest) } + href={ txLink(tx.hash, false, netVersion) } target='_blank' > { `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` } @@ -156,8 +156,13 @@ function mapStateToProps (initState) { const { accounts } = initState.personal; const accountAddresses = Object.keys(accounts); - return () => { - return { accountAddresses }; + return (state) => { + const { netVersion } = state.nodeStatus; + + return { + accountAddresses, + netVersion + }; }; } @@ -165,4 +170,3 @@ export default connect( mapStateToProps, null )(TxRow); - diff --git a/js/src/ui/TxList/TxRow/txRow.spec.js b/js/src/ui/TxList/TxRow/txRow.spec.js index dc9f4d3cc..da0c21c14 100644 --- a/js/src/ui/TxList/TxRow/txRow.spec.js +++ b/js/src/ui/TxList/TxRow/txRow.spec.js @@ -30,6 +30,9 @@ const STORE = { subscribe: sinon.stub(), getState: () => { return { + nodeStatus: { + netVersion: '42' + }, personal: { accounts: { '0x123': {} @@ -61,7 +64,7 @@ describe('ui/TxList/TxRow', () => { value: new BigNumber(1) }; - expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok; + expect(render({ address: '0x123', block, netVersion: '42', tx })).to.be.ok; }); it('renders an account link', () => { @@ -75,7 +78,7 @@ describe('ui/TxList/TxRow', () => { value: new BigNumber(1) }; - const element = render({ address: '0x123', block, isTest: true, tx }); + const element = render({ address: '0x123', block, netVersion: '42', tx }); expect(element.find('Link').prop('to')).to.equal('/accounts/0x123'); }); @@ -91,7 +94,7 @@ describe('ui/TxList/TxRow', () => { value: new BigNumber(1) }; - const element = render({ address: '0x123', block, isTest: true, tx }); + const element = render({ address: '0x123', block, netVersion: '42', tx }); expect(element.find('Link').prop('to')).to.equal('/addresses/0x456'); }); diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js index 044c6581c..f3043a47f 100644 --- a/js/src/ui/TxList/txList.js +++ b/js/src/ui/TxList/txList.js @@ -35,7 +35,7 @@ class TxList extends Component { PropTypes.array, PropTypes.object ]).isRequired, - isTest: PropTypes.bool.isRequired + netVersion: PropTypes.string.isRequired } store = new Store(this.context.api); @@ -63,7 +63,7 @@ class TxList extends Component { } renderRows () { - const { address, isTest } = this.props; + const { address, netVersion } = this.props; return this.store.sortedHashes.map((txhash) => { const tx = this.store.transactions[txhash]; @@ -76,7 +76,7 @@ class TxList extends Component { tx={ tx } block={ block } address={ address } - isTest={ isTest } + netVersion={ netVersion } /> ); }); @@ -84,10 +84,10 @@ class TxList extends Component { } function mapStateToProps (state) { - const { isTest } = state.nodeStatus; + const { netVersion } = state.nodeStatus; return { - isTest + netVersion }; } diff --git a/js/src/ui/TxList/txList.spec.js b/js/src/ui/TxList/txList.spec.js index 48ed5aac5..58a5237ac 100644 --- a/js/src/ui/TxList/txList.spec.js +++ b/js/src/ui/TxList/txList.spec.js @@ -30,7 +30,7 @@ const STORE = { getState: () => { return { nodeStatus: { - isTest: true + netVersion: '42' } }; } diff --git a/js/src/views/Account/Transactions/store.js b/js/src/views/Account/Transactions/store.js index 2ad962904..10d5af025 100644 --- a/js/src/views/Account/Transactions/store.js +++ b/js/src/views/Account/Transactions/store.js @@ -21,8 +21,8 @@ import etherscan from '~/3rdparty/etherscan'; export default class Store { @observable address = null; @observable isLoading = false; - @observable isTest = undefined; @observable isTracing = false; + @observable netVersion = '0'; @observable txHashes = []; constructor (api) { @@ -44,8 +44,8 @@ export default class Store { this.isLoading = isLoading; } - @action setTest = (isTest) => { - this.isTest = isTest; + @action setNetVersion = (netVersion) => { + this.netVersion = netVersion; } @action setTracing = (isTracing) => { @@ -55,7 +55,7 @@ export default class Store { @action updateProps = (props) => { transaction(() => { this.setAddress(props.address); - this.setTest(props.isTest); + this.setNetVersion(props.netVersion); // TODO: When tracing is enabled again, adjust to actually set this.setTracing(false && props.traceMode); @@ -65,7 +65,7 @@ export default class Store { } getTransactions () { - if (this.isTest === undefined) { + if (this.netVersion === '0') { return Promise.resolve(); } @@ -87,7 +87,7 @@ export default class Store { } fetchEtherscanTransactions () { - return etherscan.account.transactions(this.address, 0, this.isTest); + return etherscan.account.transactions(this.address, 0, false, this.netVersion); } fetchTraceTransactions () { diff --git a/js/src/views/Account/Transactions/store.spec.js b/js/src/views/Account/Transactions/store.spec.js index 04b89e1fb..c99e91512 100644 --- a/js/src/views/Account/Transactions/store.spec.js +++ b/js/src/views/Account/Transactions/store.spec.js @@ -43,7 +43,7 @@ function mockQuery () { sort: 'desc' }, reply: [{ hash: '123' }] - }], true); + }], false, '42'); } describe('views/Account/Transactions/store', () => { @@ -94,10 +94,10 @@ describe('views/Account/Transactions/store', () => { }); }); - describe('setTest', () => { - it('sets the isTest flag', () => { - store.setTest(true); - expect(store.isTest).to.be.true; + describe('setNetVersion', () => { + it('sets the netVersion', () => { + store.setNetVersion('testing'); + expect(store.netVersion).to.equal('testing'); }); }); @@ -124,7 +124,7 @@ describe('views/Account/Transactions/store', () => { it('retrieves the hashes via etherscan', () => { sinon.spy(store, 'fetchEtherscanTransactions'); store.setAddress(ADDRESS); - store.setTest(true); + store.setNetVersion('42'); store.setTracing(false); return store.getTransactions().then(() => { @@ -137,7 +137,7 @@ describe('views/Account/Transactions/store', () => { it('retrieves the hashes via tracing', () => { sinon.spy(store, 'fetchTraceTransactions'); store.setAddress(ADDRESS); - store.setTest(true); + store.setNetVersion('42'); store.setTracing(true); return store.getTransactions().then(() => { @@ -151,7 +151,7 @@ describe('views/Account/Transactions/store', () => { describe('fetchEtherscanTransactions', () => { it('retrieves the transactions', () => { store.setAddress(ADDRESS); - store.setTest(true); + store.setNetVersion('42'); return store.fetchEtherscanTransactions().then((transactions) => { expect(transactions).to.deep.equal([{ @@ -169,7 +169,7 @@ describe('views/Account/Transactions/store', () => { describe('fetchTraceTransactions', () => { it('retrieves the transactions', () => { store.setAddress(ADDRESS); - store.setTest(true); + store.setNetVersion('42'); return store.fetchTraceTransactions().then((transactions) => { expect(transactions).to.deep.equal([ diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index 1e74726c1..f36529713 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -32,7 +32,7 @@ class Transactions extends Component { static propTypes = { address: PropTypes.string.isRequired, - isTest: PropTypes.bool, + netVersion: PropTypes.string.isRequired, traceMode: PropTypes.bool } @@ -48,7 +48,7 @@ class Transactions extends Component { return; } - const hasChanged = ['isTest', 'address'] + const hasChanged = ['address', 'netVersion'] .map(key => newProps[key] !== this.props[key]) .reduce((truth, keyTruth) => truth || keyTruth, false); @@ -112,10 +112,10 @@ class Transactions extends Component { } function mapStateToProps (state) { - const { isTest, traceMode } = state.nodeStatus; + const { netVersion, traceMode } = state.nodeStatus; return { - isTest, + netVersion, traceMode }; } diff --git a/js/src/views/Account/account.test.js b/js/src/views/Account/account.test.js index 8683465fb..c88e0e156 100644 --- a/js/src/views/Account/account.test.js +++ b/js/src/views/Account/account.test.js @@ -36,7 +36,7 @@ function createRedux () { }, images: {}, nodeStatus: { - isTest: false, + netVersion: '1', traceMode: false }, personal: { diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index d9aa07089..d136029e9 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -32,8 +32,6 @@ class TabBar extends Component { }; static propTypes = { - isTest: PropTypes.bool, - netChain: PropTypes.string, pending: PropTypes.array, views: PropTypes.array.isRequired }; diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index 377dcecbc..da8a1235b 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -46,8 +46,6 @@ class Application extends Component { static propTypes = { blockNumber: PropTypes.object, children: PropTypes.node, - isTest: PropTypes.bool, - netChain: PropTypes.string, pending: PropTypes.array } @@ -86,7 +84,7 @@ class Application extends Component { } renderApp () { - const { blockNumber, children, pending, netChain, isTest } = this.props; + const { blockNumber, children, pending } = this.props; return ( <Container @@ -94,11 +92,7 @@ class Application extends Component { onCloseFirstRun={ this.store.closeFirstrun } showFirstRun={ this.store.firstrunVisible } > - <TabBar - netChain={ netChain } - isTest={ isTest } - pending={ pending } - /> + <TabBar pending={ pending } /> <div className={ styles.content }> { children } </div> @@ -125,15 +119,13 @@ class Application extends Component { } function mapStateToProps (state) { - const { blockNumber, netChain, isTest } = state.nodeStatus; + const { blockNumber } = state.nodeStatus; const { hasAccounts } = state.personal; const { pending } = state.signer; return { blockNumber, hasAccounts, - isTest, - netChain, pending }; } diff --git a/js/src/views/Contract/Events/Event/event.js b/js/src/views/Contract/Events/Event/event.js index 5e34e2dd7..64e4cdd49 100644 --- a/js/src/views/Contract/Events/Event/event.js +++ b/js/src/views/Contract/Events/Event/event.js @@ -32,7 +32,7 @@ export default class Event extends Component { static propTypes = { event: PropTypes.object.isRequired, - isTest: PropTypes.bool + netVersion: PropTypes.string.isRequired } state = { @@ -44,11 +44,11 @@ export default class Event extends Component { } render () { - const { event, isTest } = this.props; + const { event, netVersion } = this.props; const { block, transaction } = this.state; const classes = `${styles.event} ${styles[event.state]}`; - const url = txLink(event.transactionHash, isTest); + const url = txLink(event.transactionHash, false, netVersion); const keys = Object.keys(event.params).join(', '); const values = Object.keys(event.params).map((name, index) => { const param = event.params[name]; diff --git a/js/src/views/Contract/Events/events.js b/js/src/views/Contract/Events/events.js index 1731488d9..a709c137a 100644 --- a/js/src/views/Contract/Events/events.js +++ b/js/src/views/Contract/Events/events.js @@ -36,9 +36,9 @@ export default class Events extends Component { }; static propTypes = { - isTest: PropTypes.bool.isRequired, isLoading: PropTypes.bool, - events: PropTypes.array + events: PropTypes.array, + netVersion: PropTypes.string.isRequired }; static defaultProps = { @@ -47,7 +47,7 @@ export default class Events extends Component { }; render () { - const { events, isTest, isLoading } = this.props; + const { events, isLoading, netVersion } = this.props; if (isLoading) { return ( @@ -80,7 +80,7 @@ export default class Events extends Component { <Event key={ event.key } event={ event } - isTest={ isTest } + netVersion={ netVersion } /> ); }); @@ -96,7 +96,9 @@ export default class Events extends Component { </th> </tr> </thead> - <tbody>{ list }</tbody> + <tbody> + { list } + </tbody> </table> </Container> ); diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index 0a38166c4..801bd3f43 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -47,7 +47,7 @@ class Contract extends Component { accountsInfo: PropTypes.object, balances: PropTypes.object, contracts: PropTypes.object, - isTest: PropTypes.bool, + netVersion: PropTypes.string.isRequired, params: PropTypes.object }; @@ -115,7 +115,7 @@ class Contract extends Component { } render () { - const { accountsInfo, balances, contracts, params, isTest } = this.props; + const { accountsInfo, balances, contracts, netVersion, params } = this.props; const { allEvents, contract, queryValues, loadingEvents } = this.state; const account = contracts[params.address]; const balance = balances[params.address]; @@ -144,9 +144,9 @@ class Contract extends Component { values={ queryValues } /> <Events - isTest={ isTest } isLoading={ loadingEvents } events={ allEvents } + netVersion={ netVersion } /> { this.renderDetails(account) } </Page> @@ -518,14 +518,14 @@ class Contract extends Component { function mapStateToProps (state) { const { accounts, accountsInfo, contracts } = state.personal; const { balances } = state.balances; - const { isTest } = state.nodeStatus; + const { netVersion } = state.nodeStatus; return { - isTest, accounts, accountsInfo, + balances, contracts, - balances + netVersion }; } diff --git a/js/src/views/Signer/components/Account/AccountLink/accountLink.js b/js/src/views/Signer/components/Account/AccountLink/accountLink.js index 81f25f4e1..afa8e98e5 100644 --- a/js/src/views/Signer/components/Account/AccountLink/accountLink.js +++ b/js/src/views/Signer/components/Account/AccountLink/accountLink.js @@ -26,8 +26,7 @@ class AccountLink extends Component { address: PropTypes.string.isRequired, className: PropTypes.string, children: PropTypes.node, - externalLink: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired + externalLink: PropTypes.string.isRequired } state = { @@ -35,15 +34,15 @@ class AccountLink extends Component { }; componentWillMount () { - const { address, externalLink, isTest } = this.props; + const { address, externalLink } = this.props; - this.updateLink(address, externalLink, isTest); + this.updateLink(address, externalLink); } componentWillReceiveProps (nextProps) { - const { address, externalLink, isTest } = nextProps; + const { address, externalLink } = nextProps; - this.updateLink(address, externalLink, isTest); + this.updateLink(address, externalLink); } render () { @@ -71,7 +70,7 @@ class AccountLink extends Component { ); } - updateLink (address, externalLink, isTest) { + updateLink (address, externalLink) { const { accountAddresses } = this.props; const isAccount = accountAddresses.includes(address); diff --git a/js/src/views/Signer/components/Account/account.js b/js/src/views/Signer/components/Account/account.js index f3f4b66e3..5a675abf7 100644 --- a/js/src/views/Signer/components/Account/account.js +++ b/js/src/views/Signer/components/Account/account.js @@ -27,7 +27,7 @@ export default class Account extends Component { className: PropTypes.string, disabled: PropTypes.bool, externalLink: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, balance: PropTypes.object // eth BigNumber, not required since it mght take time to fetch }; @@ -53,14 +53,14 @@ export default class Account extends Component { } render () { - const { address, disabled, externalLink, isTest, className } = this.props; + const { address, className, disabled, externalLink, netVersion } = this.props; return ( <div className={ `${styles.acc} ${className}` }> <AccountLink address={ address } externalLink={ externalLink } - isTest={ isTest } + netVersion={ netVersion } > <IdentityIcon center @@ -83,7 +83,7 @@ export default class Account extends Component { } renderName () { - const { address, externalLink, isTest } = this.props; + const { address, externalLink, netVersion } = this.props; const name = <IdentityName address={ address } empty />; if (!name) { @@ -91,7 +91,7 @@ export default class Account extends Component { <AccountLink address={ address } externalLink={ externalLink } - isTest={ isTest } + netVersion={ netVersion } > [{ this.shortAddress(address) }] </AccountLink> @@ -102,7 +102,7 @@ export default class Account extends Component { <AccountLink address={ address } externalLink={ externalLink } - isTest={ isTest } + netVersion={ netVersion } > <span> <span className={ styles.name }>{ name }</span> diff --git a/js/src/views/Signer/components/RequestPending/requestPending.js b/js/src/views/Signer/components/RequestPending/requestPending.js index 181d12462..671df6cbb 100644 --- a/js/src/views/Signer/components/RequestPending/requestPending.js +++ b/js/src/views/Signer/components/RequestPending/requestPending.js @@ -27,7 +27,7 @@ export default class RequestPending extends Component { gasLimit: PropTypes.object.isRequired, id: PropTypes.object.isRequired, isSending: PropTypes.bool.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, onConfirm: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, origin: PropTypes.object.isRequired, @@ -45,7 +45,7 @@ export default class RequestPending extends Component { }; render () { - const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, signerstore, origin } = this.props; + const { className, date, focus, gasLimit, id, isSending, netVersion, onReject, payload, signerstore, origin } = this.props; if (payload.sign) { const { sign } = payload; @@ -59,7 +59,7 @@ export default class RequestPending extends Component { id={ id } isFinished={ false } isSending={ isSending } - isTest={ isTest } + netVersion={ netVersion } onConfirm={ this.onConfirm } onReject={ onReject } origin={ origin } @@ -79,7 +79,7 @@ export default class RequestPending extends Component { gasLimit={ gasLimit } id={ id } isSending={ isSending } - isTest={ isTest } + netVersion={ netVersion } onConfirm={ this.onConfirm } onReject={ onReject } origin={ origin } diff --git a/js/src/views/Signer/components/RequestPending/requestPending.spec.js b/js/src/views/Signer/components/RequestPending/requestPending.spec.js index e21662fcf..686c23b67 100644 --- a/js/src/views/Signer/components/RequestPending/requestPending.spec.js +++ b/js/src/views/Signer/components/RequestPending/requestPending.spec.js @@ -54,8 +54,8 @@ function render (payload) { date={ new Date() } gasLimit={ new BigNumber(100000) } id={ new BigNumber(123) } - isTest={ false } isSending={ false } + netVersion='42' onConfirm={ onConfirm } onReject={ onReject } origin={ {} } diff --git a/js/src/views/Signer/components/SignRequest/signRequest.js b/js/src/views/Signer/components/SignRequest/signRequest.js index cc235aa58..373262d41 100644 --- a/js/src/views/Signer/components/SignRequest/signRequest.js +++ b/js/src/views/Signer/components/SignRequest/signRequest.js @@ -46,7 +46,7 @@ export default class SignRequest extends Component { data: PropTypes.string.isRequired, id: PropTypes.object.isRequired, isFinished: PropTypes.bool.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, signerstore: PropTypes.object.isRequired, className: PropTypes.string, @@ -106,7 +106,7 @@ export default class SignRequest extends Component { renderDetails () { const { api } = this.context; - const { address, isTest, signerstore, data, origin } = this.props; + const { address, data, netVersion, origin, signerstore } = this.props; const { balances, externalLink } = signerstore; const balance = balances[address]; @@ -122,7 +122,7 @@ export default class SignRequest extends Component { address={ address } balance={ balance } externalLink={ externalLink } - isTest={ isTest } + netVersion={ netVersion } /> <RequestOrigin origin={ origin } /> </div> diff --git a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js index c255fa0c3..ebbddf895 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js +++ b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js @@ -36,7 +36,7 @@ export default class TransactionMainDetails extends Component { fromBalance: PropTypes.object, gasStore: PropTypes.object, id: PropTypes.object.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, origin: PropTypes.any, totalValue: PropTypes.object.isRequired, transaction: PropTypes.object.isRequired, @@ -63,7 +63,7 @@ export default class TransactionMainDetails extends Component { } render () { - const { children, disabled, externalLink, from, fromBalance, gasStore, isTest, transaction, origin } = this.props; + const { children, disabled, externalLink, from, fromBalance, gasStore, netVersion, transaction, origin } = this.props; return ( <div className={ styles.transaction }> @@ -74,7 +74,7 @@ export default class TransactionMainDetails extends Component { balance={ fromBalance } disabled={ disabled } externalLink={ externalLink } - isTest={ isTest } + netVersion={ netVersion } /> </div> <RequestOrigin origin={ origin } /> diff --git a/js/src/views/Signer/components/TransactionPending/transactionPending.js b/js/src/views/Signer/components/TransactionPending/transactionPending.js index 49b9c3ef8..9b0b91ef6 100644 --- a/js/src/views/Signer/components/TransactionPending/transactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.js @@ -43,7 +43,7 @@ class TransactionPending extends Component { gasLimit: PropTypes.object, id: PropTypes.object.isRequired, isSending: PropTypes.bool.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, nonce: PropTypes.number, onConfirm: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, @@ -98,7 +98,7 @@ class TransactionPending extends Component { } renderTransaction () { - const { accounts, className, focus, id, isSending, isTest, signerstore, transaction, origin } = this.props; + const { accounts, className, focus, id, isSending, netVersion, origin, signerstore, transaction } = this.props; const { totalValue } = this.state; const { balances, externalLink } = signerstore; const { from, value } = transaction; @@ -116,7 +116,7 @@ class TransactionPending extends Component { fromBalance={ fromBalance } gasStore={ this.gasStore } id={ id } - isTest={ isTest } + netVersion={ netVersion } origin={ origin } totalValue={ totalValue } transaction={ transaction } diff --git a/js/src/views/Signer/components/TxHashLink/txHashLink.js b/js/src/views/Signer/components/TxHashLink/txHashLink.js index 629f30835..42cc336e7 100644 --- a/js/src/views/Signer/components/TxHashLink/txHashLink.js +++ b/js/src/views/Signer/components/TxHashLink/txHashLink.js @@ -22,17 +22,17 @@ export default class TxHashLink extends Component { static propTypes = { children: PropTypes.node, className: PropTypes.string, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, txHash: PropTypes.string.isRequired } render () { - const { children, className, isTest, txHash } = this.props; + const { children, className, netVersion, txHash } = this.props; return ( <a className={ className } - href={ txLink(txHash, isTest) } + href={ txLink(txHash, false, netVersion) } target='_blank' > { children || txHash } diff --git a/js/src/views/Signer/containers/Embedded/embedded.js b/js/src/views/Signer/containers/Embedded/embedded.js index ebb3d8e2d..6d63590c7 100644 --- a/js/src/views/Signer/containers/Embedded/embedded.js +++ b/js/src/views/Signer/containers/Embedded/embedded.js @@ -39,7 +39,7 @@ class Embedded extends Component { }).isRequired, externalLink: PropTypes.string, gasLimit: PropTypes.object.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, signer: PropTypes.shape({ finished: PropTypes.array.isRequired, pending: PropTypes.array.isRequired @@ -80,7 +80,7 @@ class Embedded extends Component { } renderPending = (data, index) => { - const { actions, gasLimit, isTest } = this.props; + const { actions, gasLimit, netVersion } = this.props; const { date, id, isSending, payload, origin } = data; return ( @@ -91,7 +91,7 @@ class Embedded extends Component { gasLimit={ gasLimit } id={ id } isSending={ isSending } - isTest={ isTest } + netVersion={ netVersion } key={ id } onConfirm={ actions.startConfirmRequest } onReject={ actions.startRejectRequest } @@ -108,13 +108,13 @@ class Embedded extends Component { } function mapStateToProps (state) { - const { gasLimit, isTest } = state.nodeStatus; + const { gasLimit, netVersion } = state.nodeStatus; const { actions, signer } = state; return { actions, gasLimit, - isTest, + netVersion, signer }; } diff --git a/js/src/views/Signer/containers/RequestsPage/requestsPage.js b/js/src/views/Signer/containers/RequestsPage/requestsPage.js index deb8560f7..d90ed7693 100644 --- a/js/src/views/Signer/containers/RequestsPage/requestsPage.js +++ b/js/src/views/Signer/containers/RequestsPage/requestsPage.js @@ -40,7 +40,7 @@ class RequestsPage extends Component { startRejectRequest: PropTypes.func.isRequired }).isRequired, gasLimit: PropTypes.object.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, signer: PropTypes.shape({ pending: PropTypes.array.isRequired, finished: PropTypes.array.isRequired @@ -106,7 +106,7 @@ class RequestsPage extends Component { } renderPending = (data, index) => { - const { actions, gasLimit, isTest } = this.props; + const { actions, gasLimit, netVersion } = this.props; const { date, id, isSending, payload, origin } = data; return ( @@ -117,7 +117,7 @@ class RequestsPage extends Component { gasLimit={ gasLimit } id={ id } isSending={ isSending } - isTest={ isTest } + netVersion={ netVersion } key={ id } onConfirm={ actions.startConfirmRequest } onReject={ actions.startRejectRequest } @@ -130,13 +130,13 @@ class RequestsPage extends Component { } function mapStateToProps (state) { - const { gasLimit, isTest } = state.nodeStatus; + const { gasLimit, netVersion } = state.nodeStatus; const { actions, signer } = state; return { actions, gasLimit, - isTest, + netVersion, signer }; } diff --git a/js/src/views/Wallet/Confirmations/confirmations.js b/js/src/views/Wallet/Confirmations/confirmations.js index 8f7e6ea52..bea1ffd95 100644 --- a/js/src/views/Wallet/Confirmations/confirmations.js +++ b/js/src/views/Wallet/Confirmations/confirmations.js @@ -37,7 +37,7 @@ class WalletConfirmations extends Component { static propTypes = { accounts: PropTypes.object.isRequired, address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, owners: PropTypes.array.isRequired, require: PropTypes.object.isRequired, confirmOperation: PropTypes.func.isRequired, @@ -115,7 +115,7 @@ class WalletConfirmation extends Component { accounts: PropTypes.object.isRequired, confirmation: PropTypes.object.isRequired, address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, owners: PropTypes.array.isRequired, require: PropTypes.object.isRequired, confirmOperation: PropTypes.func.isRequired, @@ -353,7 +353,7 @@ class WalletConfirmation extends Component { } renderTransactionRow (confirmation, className) { - const { address, isTest } = this.props; + const { address, netVersion } = this.props; const { operation, transactionHash, blockNumber, value, to, data } = confirmation; if (value && to && data) { @@ -362,7 +362,7 @@ class WalletConfirmation extends Component { address={ address } className={ className } historic={ false } - isTest={ isTest } + netVersion={ netVersion } key={ operation } tx={ { hash: transactionHash, diff --git a/js/src/views/Wallet/Transactions/transactions.js b/js/src/views/Wallet/Transactions/transactions.js index d81c2633a..0ef853a70 100644 --- a/js/src/views/Wallet/Transactions/transactions.js +++ b/js/src/views/Wallet/Transactions/transactions.js @@ -26,7 +26,7 @@ import txListStyles from '~/ui/TxList/txList.css'; export default class WalletTransactions extends Component { static propTypes = { address: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, transactions: PropTypes.array }; @@ -51,7 +51,7 @@ export default class WalletTransactions extends Component { ); } renderTransactions () { - const { address, isTest, transactions } = this.props; + const { address, netVersion, transactions } = this.props; if (!transactions) { return null; @@ -76,7 +76,7 @@ export default class WalletTransactions extends Component { return ( <TxRow address={ address } - isTest={ isTest } + netVersion={ netVersion } key={ `${transactionHash}_${index}` } tx={ { blockNumber, diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js index 61d80ce69..84d4dbe5b 100644 --- a/js/src/views/Wallet/wallet.js +++ b/js/src/views/Wallet/wallet.js @@ -37,13 +37,13 @@ import styles from './wallet.css'; class WalletContainer extends Component { static propTypes = { - isTest: PropTypes.any + netVersion: PropTypes.string.isRequired }; render () { - const { isTest, ...others } = this.props; + const { netVersion, ...others } = this.props; - if (isTest !== false && isTest !== true) { + if (netVersion === '0') { return ( <Loading size={ 4 } /> ); @@ -51,7 +51,7 @@ class WalletContainer extends Component { return ( <Wallet - isTest={ isTest } + netVersion={ netVersion } { ...others } /> ); @@ -66,7 +66,7 @@ class Wallet extends Component { static propTypes = { address: PropTypes.string.isRequired, balance: nullableProptype(PropTypes.object.isRequired), - isTest: PropTypes.bool.isRequired, + netVersion: PropTypes.string.isRequired, owned: PropTypes.bool.isRequired, setVisibleAccounts: PropTypes.func.isRequired, wallet: PropTypes.object.isRequired, @@ -181,7 +181,7 @@ class Wallet extends Component { } renderDetails () { - const { address, isTest, wallet } = this.props; + const { address, netVersion, wallet } = this.props; const { owners, require, confirmations, transactions } = wallet; if (!owners || !require) { @@ -196,7 +196,7 @@ class Wallet extends Component { <WalletConfirmations address={ address } confirmations={ confirmations } - isTest={ isTest } + netVersion={ netVersion } key='confirmations' owners={ owners } require={ require } @@ -204,7 +204,7 @@ class Wallet extends Component { <WalletTransactions address={ address } - isTest={ isTest } + netVersion={ netVersion } key='transactions' transactions={ transactions } /> @@ -389,7 +389,7 @@ function mapStateToProps (_, initProps) { const { address } = initProps.params; return (state) => { - const { isTest } = state.nodeStatus; + const { netVersion } = state.nodeStatus; const { accountsInfo = {}, accounts = {} } = state.personal; const { balances } = state.balances; const walletAccount = accounts[address] || accountsInfo[address] || null; @@ -405,7 +405,7 @@ function mapStateToProps (_, initProps) { return { address, balance, - isTest, + netVersion, owned, wallet, walletAccount From a02063f3484f5bc14b7857728a4f2bf3e8849ddb Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Mon, 6 Mar 2017 08:08:05 +0000 Subject: [PATCH 61/93] [ci skip] js-precompiled 20170306-080251 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c578977b..4034d900c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#377749694cfebdea96cca5ed7ff777eef1424b66" +source = "git+https://github.com/ethcore/js-precompiled.git#aac9c95b4154ddbbc3b330aad8eeff34c327bee5" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index dab289769..f895cc74e 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.117", + "version": "0.3.118", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 4d203d563ca48a4b37f852a02c0147197589d532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 6 Mar 2017 10:32:41 +0100 Subject: [PATCH 62/93] Rephrasing token generation screen. (#4777) --- js/src/i18n/_default/connection.js | 2 +- js/src/views/Connection/connection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/i18n/_default/connection.js b/js/src/i18n/_default/connection.js index 10f330618..e51943178 100644 --- a/js/src/i18n/_default/connection.js +++ b/js/src/i18n/_default/connection.js @@ -18,7 +18,7 @@ export default { connectingAPI: `Connecting to the Parity Secure API.`, connectingNode: `Connecting to the Parity Node. If this informational message persists, please ensure that your Parity node is running and reachable on the network.`, invalidToken: `invalid signer token`, - noConnection: `Unable to make a connection to the Parity Secure API. To update your secure token or to generate a new one, run {newToken} and supply the token below`, + noConnection: `Unable to make a connection to the Parity Secure API. To update your secure token or to generate a new one, run {newToken} and paste the generated token into the space below.`, token: { hint: `a generated token from Parity`, label: `secure token` diff --git a/js/src/views/Connection/connection.js b/js/src/views/Connection/connection.js index 710304a05..788385b89 100644 --- a/js/src/views/Connection/connection.js +++ b/js/src/views/Connection/connection.js @@ -88,7 +88,7 @@ class Connection extends Component { <div> <FormattedMessage id='connection.noConnection' - defaultMessage='Unable to make a connection to the Parity Secure API. To update your secure token or to generate a new one, run {newToken} and supply the token below' + defaultMessage='Unable to make a connection to the Parity Secure API. To update your secure token or to generate a new one, run {newToken} and paste the generated token into the space below.' values={ { newToken: <span className={ styles.console }>parity signer new-token</span> } } From 0de87710074292206cf5d1ba65c1cede098a0347 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Mon, 6 Mar 2017 09:45:14 +0000 Subject: [PATCH 63/93] [ci skip] js-precompiled 20170306-093959 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4034d900c..a754b2c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#aac9c95b4154ddbbc3b330aad8eeff34c327bee5" +source = "git+https://github.com/ethcore/js-precompiled.git#ede2e14caac0e3de10a1aa92daff874ab7f278d6" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index f895cc74e..c87338f50 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.118", + "version": "0.3.119", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 4f934cf2c2b242d710bfe584cb857ca162fd238e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 6 Mar 2017 15:11:39 +0100 Subject: [PATCH 64/93] Optimize signature for fallback function. (#4780) --- js/src/abi/util/signature.js | 3 ++- js/src/abi/util/signature.spec.js | 8 ++++---- js/src/api/util/encode.js | 4 +--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/js/src/abi/util/signature.js b/js/src/abi/util/signature.js index ccc7cc062..86ed6f265 100644 --- a/js/src/abi/util/signature.js +++ b/js/src/abi/util/signature.js @@ -21,8 +21,9 @@ export function eventSignature (eventName, params) { const { strName, name } = parseName(eventName); const types = (params || []).map(fromParamType).join(','); const id = `${strName}(${types})`; + const signature = strName ? keccak_256(id) : ''; - return { id, name, signature: keccak_256(id) }; + return { id, name, signature }; } export function methodSignature (methodName, params) { diff --git a/js/src/abi/util/signature.spec.js b/js/src/abi/util/signature.spec.js index 1e9b7a9ee..118ebf4e5 100644 --- a/js/src/abi/util/signature.spec.js +++ b/js/src/abi/util/signature.spec.js @@ -46,7 +46,7 @@ describe('abi/util/signature', () => { expect(eventSignature(undefined, [])).to.deep.equal({ id: '()', name: undefined, - signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe' + signature: '' }); }); @@ -54,7 +54,7 @@ describe('abi/util/signature', () => { expect(eventSignature(undefined, undefined)).to.deep.equal({ id: '()', name: undefined, - signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe' + signature: '' }); }); }); @@ -96,7 +96,7 @@ describe('abi/util/signature', () => { expect(methodSignature(undefined, [])).to.deep.equal({ id: '()', name: undefined, - signature: '861731d5' + signature: '' }); }); @@ -104,7 +104,7 @@ describe('abi/util/signature', () => { expect(methodSignature(undefined, undefined)).to.deep.equal({ id: '()', name: undefined, - signature: '861731d5' + signature: '' }); }); }); diff --git a/js/src/api/util/encode.js b/js/src/api/util/encode.js index d727d1e63..5b5fb5eac 100644 --- a/js/src/api/util/encode.js +++ b/js/src/api/util/encode.js @@ -34,7 +34,5 @@ export function abiEncode (methodName, inputTypes, data) { }) }, data); - return methodName === null - ? `0x${result.substr(10)}` - : result; + return result; } From 24aac8fa8945508f94367182976f4d7723a0c33c Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Mon, 6 Mar 2017 14:26:42 +0000 Subject: [PATCH 65/93] [ci skip] js-precompiled 20170306-142052 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a754b2c07..0a6750355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#ede2e14caac0e3de10a1aa92daff874ab7f278d6" +source = "git+https://github.com/ethcore/js-precompiled.git#94da980fb81d6145e38ca87d37a9137e8440086a" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index c87338f50..d78d14139 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.119", + "version": "0.3.120", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 38884cdf825313880cc1d3044d019376cffe691b Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Mon, 6 Mar 2017 22:44:41 +0400 Subject: [PATCH 66/93] update docker-build add arg [ci skip] --- scripts/docker-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index 9c874eac6..b9724cd2f 100644 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -1,4 +1,4 @@ #!/bin/bash cd docker/hub -docker build --no-cache=true --tag ethcore/parity:$1 . +docker build --build-arg BUILD_TAG=$1 --no-cache=true --tag ethcore/parity:$1 . docker push ethcore/parity:$1 From 344b5b120f5b7b0abb9782e9d17bf217f9ef93c1 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Mon, 6 Mar 2017 22:46:35 +0400 Subject: [PATCH 67/93] update Dockerfile for hub add BUILD_TAG ARG [ci skip] --- docker/hub/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/hub/Dockerfile b/docker/hub/Dockerfile index 8973ed63f..81ec92133 100644 --- a/docker/hub/Dockerfile +++ b/docker/hub/Dockerfile @@ -1,6 +1,10 @@ FROM ubuntu:14.04 MAINTAINER Parity Technologies <devops@parity.io> WORKDIR /build +#ENV for build TAG +ARG BUILD_TAG +ENV BUILD_TAG ${BUILD_TAG:-master} +RUN echo $BUILD_TAG # install tools and dependencies RUN apt-get update && \ apt-get install -y --force-yes --no-install-recommends \ @@ -47,7 +51,7 @@ RUN apt-get update && \ cd /build&&git clone https://github.com/ethcore/parity && \ cd parity && \ git pull&& \ - git checkout $CI_BUILD_REF_NAME && \ + git checkout $BUILD_TAG && \ cargo build --verbose --release --features final && \ #ls /build/parity/target/release/parity && \ strip /build/parity/target/release/parity && \ From cec37207beaccc7d2d2d4a6feea5619efaa21933 Mon Sep 17 00:00:00 2001 From: Gav Wood <gavin@parity.io> Date: Mon, 6 Mar 2017 21:37:38 +0100 Subject: [PATCH 68/93] Update comments and reg ABI (#4787) * Update comments. * Fix up new ABI. --- ethcore/src/client/registry.rs | 228 ++++++++++++++++++++++----------- ethcore/src/ethereum/mod.rs | 20 +-- scripts/contractABI.js | 12 +- 3 files changed, 169 insertions(+), 91 deletions(-) diff --git a/ethcore/src/client/registry.rs b/ethcore/src/client/registry.rs index 9e60a1251..c8f750576 100644 --- a/ethcore/src/client/registry.rs +++ b/ethcore/src/client/registry.rs @@ -1,264 +1,338 @@ // Autogenerated from JSON contract definition using Rust contract convertor. - +// Command line: --name=Registry --jsonabi=/Users/gav/registry.abi +#![allow(unused_imports)] use std::string::String; use std::result::Result; use std::fmt; use {util, ethabi}; -use util::FixedHash; -use util::Uint; +use util::{FixedHash, Uint}; pub struct Registry { contract: ethabi::Contract, pub address: util::Address, - do_call: Box<Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + 'static>, + do_call: Box<Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static>, } impl Registry { - pub fn new<F>(address: util::Address, do_call: F) -> Self where F: Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + 'static { + pub fn new<F>(address: util::Address, do_call: F) -> Self + where F: Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static { Registry { - contract: ethabi::Contract::new(ethabi::Interface::load(b"[{\"constant\":false,\"inputs\":[{\"name\":\"_new\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"}],\"name\":\"confirmReverse\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"reserve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"bytes32\"}],\"name\":\"set\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"drop\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"getAddress\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"setFee\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"reserved\",\"outputs\":[{\"name\":\"reserved\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"drain\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"},{\"name\":\"_who\",\"type\":\"address\"}],\"name\":\"proposeReverse\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"getUint\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"get\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"fee\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"getOwner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"reverse\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"setUint\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"removeReverse\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"address\"}],\"name\":\"setAddress\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Drained\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"FeeChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"Reserved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"Transferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"Dropped\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"key\",\"type\":\"string\"},{\"indexed\":false,\"name\":\"plainKey\",\"type\":\"string\"}],\"name\":\"DataChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"string\"},{\"indexed\":true,\"name\":\"reverse\",\"type\":\"address\"}],\"name\":\"ReverseProposed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"string\"},{\"indexed\":true,\"name\":\"reverse\",\"type\":\"address\"}],\"name\":\"ReverseConfirmed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"name\",\"type\":\"string\"},{\"indexed\":true,\"name\":\"reverse\",\"type\":\"address\"}],\"name\":\"ReverseRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"old\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"current\",\"type\":\"address\"}],\"name\":\"NewOwner\",\"type\":\"event\"}]").expect("JSON is autogenerated; qed")), + contract: ethabi::Contract::new(ethabi::Interface::load(b"[{\"constant\":true,\"inputs\":[{\"name\":\"_data\",\"type\":\"address\"}],\"name\":\"canReverse\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_new\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"bytes32\"}],\"name\":\"setData\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"}],\"name\":\"confirmReverse\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"reserve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":true,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"drop\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"getAddress\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"setFee\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"getData\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"reserved\",\"outputs\":[{\"name\":\"reserved\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"drain\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"},{\"name\":\"_who\",\"type\":\"address\"}],\"name\":\"proposeReverse\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"hasReverse\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"}],\"name\":\"getUint\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"fee\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"getOwner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"}],\"name\":\"getReverse\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_data\",\"type\":\"address\"}],\"name\":\"reverse\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"setUint\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"},{\"name\":\"_who\",\"type\":\"address\"}],\"name\":\"confirmReverseAs\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"removeReverse\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"bytes32\"},{\"name\":\"_key\",\"type\":\"string\"},{\"name\":\"_value\",\"type\":\"address\"}],\"name\":\"setAddress\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"}]").expect("JSON is autogenerated; qed")), address: address, do_call: Box::new(do_call), } } fn as_string<T: fmt::Debug>(e: T) -> String { format!("{:?}", e) } + /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_data","type":"address"}],"name":"canReverse","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn can_reverse(&self, _data: &util::Address) -> Result<bool, String> + { + let call = self.contract.function("canReverse".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::Address(_data.clone().0)] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + } + /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn set_owner(&self, _new: &util::Address) -> Result<(), String> { + pub fn set_owner(&self, _new: &util::Address) -> Result<(), String> + { let call = self.contract.function("setOwner".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::Address(_new.clone().0)] ).map_err(Self::as_string)?; call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - Ok(()) + Ok(()) + } + + /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"bytes32"}],"name":"setData","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn set_data(&self, _name: &util::H256, _key: &str, _value: &util::H256) -> Result<bool, String> + { + let call = self.contract.function("setData".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned()), ethabi::Token::FixedBytes(_value.as_ref().to_owned())] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"string"}],"name":"confirmReverse","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn confirm_reverse(&self, _name: &str) -> Result<bool, String> { + pub fn confirm_reverse(&self, _name: &str) -> Result<bool, String> + { let call = self.contract.function("confirmReverse".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::String(_name.to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } - /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` + /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserve","outputs":[{"name":"success","type":"bool"}],"payable":true,"type":"function"}` #[allow(dead_code)] - pub fn reserve(&self, _name: &util::H256) -> Result<bool, String> { + pub fn reserve(&self, _name: &util::H256) -> Result<bool, String> + { let call = self.contract.function("reserve".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) - } - - /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"bytes32"}],"name":"set","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` - #[allow(dead_code)] - pub fn set(&self, _name: &util::H256, _key: &str, _value: &util::H256) -> Result<bool, String> { - let call = self.contract.function("set".into()).map_err(Self::as_string)?; - let data = call.encode_call( - vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned()), ethabi::Token::FixedBytes(_value.as_ref().to_owned())] - ).map_err(Self::as_string)?; - let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"drop","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn drop(&self, _name: &util::H256) -> Result<bool, String> { + pub fn drop(&self, _name: &util::H256) -> Result<bool, String> + { let call = self.contract.function("drop".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn get_address(&self, _name: &util::H256, _key: &str) -> Result<util::Address, String> { + pub fn get_address(&self, _name: &util::H256, _key: &str) -> Result<util::Address, String> + { let call = self.contract.function("getAddress".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) } - /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_amount","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"}` + /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_amount","type":"uint256"}],"name":"setFee","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn set_fee(&self, _amount: util::U256) -> Result<(), String> { + pub fn set_fee(&self, _amount: util::U256) -> Result<bool, String> + { let call = self.contract.function("setFee".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::Uint({ let mut r = [0u8; 32]; _amount.to_big_endian(&mut r); r })] ).map_err(Self::as_string)?; - call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - - Ok(()) + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_to","type":"address"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn transfer(&self, _name: &util::H256, _to: &util::Address) -> Result<bool, String> { + pub fn transfer(&self, _name: &util::H256, _to: &util::Address) -> Result<bool, String> + { let call = self.contract.function("transfer".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::Address(_to.clone().0)] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn owner(&self) -> Result<util::Address, String> { + pub fn owner(&self) -> Result<util::Address, String> + { let call = self.contract.function("owner".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) + } + + /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"getData","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn get_data(&self, _name: &util::H256, _key: &str) -> Result<util::H256, String> + { + let call = self.contract.function("getData".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned())] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_fixed_bytes().ok_or("Invalid type returned")?; util::H256::from_slice(r.as_ref()) })) } /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserved","outputs":[{"name":"reserved","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn reserved(&self, _name: &util::H256) -> Result<bool, String> { + pub fn reserved(&self, _name: &util::H256) -> Result<bool, String> + { let call = self.contract.function("reserved".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } - /// Auto-generated from: `{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"}` + /// Auto-generated from: `{"constant":false,"inputs":[],"name":"drain","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn drain(&self) -> Result<(), String> { + pub fn drain(&self) -> Result<bool, String> + { let call = self.contract.function("drain".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![] ).map_err(Self::as_string)?; - call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - - Ok(()) + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_who","type":"address"}],"name":"proposeReverse","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn propose_reverse(&self, _name: &str, _who: &util::Address) -> Result<bool, String> { + pub fn propose_reverse(&self, _name: &str, _who: &util::Address) -> Result<bool, String> + { let call = self.contract.function("proposeReverse".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::String(_name.to_owned()), ethabi::Token::Address(_who.clone().0)] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + } + + /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"hasReverse","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn has_reverse(&self, _name: &util::H256) -> Result<bool, String> + { + let call = self.contract.function("hasReverse".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn get_uint(&self, _name: &util::H256, _key: &str) -> Result<util::U256, String> { + pub fn get_uint(&self, _name: &util::H256, _key: &str) -> Result<util::U256, String> + { let call = self.contract.function("getUint".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_uint().ok_or("Invalid type returned")?; util::U256::from(r.as_ref()) })) - } - - /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"}` - #[allow(dead_code)] - pub fn get(&self, _name: &util::H256, _key: &str) -> Result<util::H256, String> { - let call = self.contract.function("get".into()).map_err(Self::as_string)?; - let data = call.encode_call( - vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned())] - ).map_err(Self::as_string)?; - let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_fixed_bytes().ok_or("Invalid type returned")?; util::H256::from_slice(r.as_ref()) })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_uint().ok_or("Invalid type returned")?; util::U256::from(r.as_ref()) })) } /// Auto-generated from: `{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn fee(&self) -> Result<util::U256, String> { + pub fn fee(&self) -> Result<util::U256, String> + { let call = self.contract.function("fee".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_uint().ok_or("Invalid type returned")?; util::U256::from(r.as_ref()) })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_uint().ok_or("Invalid type returned")?; util::U256::from(r.as_ref()) })) } /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn get_owner(&self, _name: &util::H256) -> Result<util::Address, String> { + pub fn get_owner(&self, _name: &util::H256) -> Result<util::Address, String> + { let call = self.contract.function("getOwner".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) } - /// Auto-generated from: `{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"reverse","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"}` + /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"getReverse","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn reverse(&self, _1: &util::Address) -> Result<String, String> { - let call = self.contract.function("reverse".into()).map_err(Self::as_string)?; + pub fn get_reverse(&self, _name: &util::H256) -> Result<util::Address, String> + { + let call = self.contract.function("getReverse".into()).map_err(Self::as_string)?; let data = call.encode_call( - vec![ethabi::Token::Address(_1.clone().0)] + vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned())] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_string().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_address().ok_or("Invalid type returned")?; util::Address::from(r) })) + } + + /// Auto-generated from: `{"constant":true,"inputs":[{"name":"_data","type":"address"}],"name":"reverse","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn reverse(&self, _data: &util::Address) -> Result<String, String> + { + let call = self.contract.function("reverse".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::Address(_data.clone().0)] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_string().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"uint256"}],"name":"setUint","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn set_uint(&self, _name: &util::H256, _key: &str, _value: util::U256) -> Result<bool, String> { + pub fn set_uint(&self, _name: &util::H256, _key: &str, _value: util::U256) -> Result<bool, String> + { let call = self.contract.function("setUint".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned()), ethabi::Token::Uint({ let mut r = [0u8; 32]; _value.to_big_endian(&mut r); r })] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + } + + /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_who","type":"address"}],"name":"confirmReverseAs","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` + #[allow(dead_code)] + pub fn confirm_reverse_as(&self, _name: &str, _who: &util::Address) -> Result<bool, String> + { + let call = self.contract.function("confirmReverseAs".into()).map_err(Self::as_string)?; + let data = call.encode_call( + vec![ethabi::Token::String(_name.to_owned()), ethabi::Token::Address(_who.clone().0)] + ).map_err(Self::as_string)?; + let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; + let mut result = output.into_iter().rev().collect::<Vec<_>>(); + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } /// Auto-generated from: `{"constant":false,"inputs":[],"name":"removeReverse","outputs":[],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn remove_reverse(&self) -> Result<(), String> { + pub fn remove_reverse(&self) -> Result<(), String> + { let call = self.contract.function("removeReverse".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![] ).map_err(Self::as_string)?; call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - Ok(()) + Ok(()) } /// Auto-generated from: `{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"address"}],"name":"setAddress","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"}` #[allow(dead_code)] - pub fn set_address(&self, _name: &util::H256, _key: &str, _value: &util::Address) -> Result<bool, String> { + pub fn set_address(&self, _name: &util::H256, _key: &str, _value: &util::Address) -> Result<bool, String> + { let call = self.contract.function("setAddress".into()).map_err(Self::as_string)?; let data = call.encode_call( vec![ethabi::Token::FixedBytes(_name.as_ref().to_owned()), ethabi::Token::String(_key.to_owned()), ethabi::Token::Address(_value.clone().0)] ).map_err(Self::as_string)?; let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) + Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_bool().ok_or("Invalid type returned")?; r })) } -} \ No newline at end of file +} + diff --git a/ethcore/src/ethereum/mod.rs b/ethcore/src/ethereum/mod.rs index f045bfd0d..ce8b84b31 100644 --- a/ethcore/src/ethereum/mod.rs +++ b/ethcore/src/ethereum/mod.rs @@ -42,13 +42,13 @@ fn load(b: &[u8]) -> Spec { Spec::load(b).expect("chain spec is invalid") } -/// Create a new Olympic chain spec. +/// Create a new Foundation Olympic chain spec. pub fn new_olympic() -> Spec { load(include_bytes!("../../res/ethereum/olympic.json")) } -/// Create a new Frontier mainnet chain spec. +/// Create a new Foundation Mainnet chain spec. pub fn new_foundation() -> Spec { load(include_bytes!("../../res/ethereum/foundation.json")) } -/// Create a new Frontier mainnet chain spec without the DAO hardfork. +/// Create a new Classic Mainnet chain spec without the DAO hardfork. pub fn new_classic() -> Spec { load(include_bytes!("../../res/ethereum/classic.json")) } /// Create a new Expanse mainnet chain spec. @@ -57,25 +57,25 @@ pub fn new_expanse() -> Spec { load(include_bytes!("../../res/ethereum/expanse.j /// Create a new Kovan testnet chain spec. pub fn new_kovan() -> Spec { load(include_bytes!("../../res/ethereum/kovan.json")) } -/// Create a new Frontier chain spec as though it never changes to Homestead. +/// Create a new Foundation Frontier-era chain spec as though it never changes to Homestead. pub fn new_frontier_test() -> Spec { load(include_bytes!("../../res/ethereum/frontier_test.json")) } -/// Create a new Homestead chain spec as though it never changed from Frontier. +/// Create a new Foundation Homestead-era chain spec as though it never changed from Frontier. pub fn new_homestead_test() -> Spec { load(include_bytes!("../../res/ethereum/homestead_test.json")) } -/// Create a new Homestead-EIP150 chain spec as though it never changed from Homestead/Frontier. +/// Create a new Foundation Homestead-EIP150-era chain spec as though it never changed from Homestead/Frontier. pub fn new_eip150_test() -> Spec { load(include_bytes!("../../res/ethereum/eip150_test.json")) } -/// Create a new Homestead-EIP150 chain spec as though it never changed from Homestead/Frontier. +/// Create a new Foundation Homestead-EIP161-era chain spec as though it never changed from Homestead/Frontier. pub fn new_eip161_test() -> Spec { load(include_bytes!("../../res/ethereum/eip161_test.json")) } -/// Create a new Frontier/Homestead/DAO chain spec with transition points at #5 and #8. +/// Create a new Foundation Frontier/Homestead/DAO chain spec with transition points at #5 and #8. pub fn new_transition_test() -> Spec { load(include_bytes!("../../res/ethereum/transition_test.json")) } -/// Create a new Frontier main net chain spec without genesis accounts. +/// Create a new Foundation Mainnet chain spec without genesis accounts. pub fn new_mainnet_like() -> Spec { load(include_bytes!("../../res/ethereum/frontier_like_test.json")) } -/// Create a new Ropsten chain spec. +/// Create a new Foundation Ropsten chain spec. pub fn new_ropsten() -> Spec { load(include_bytes!("../../res/ethereum/ropsten.json")) } /// Create a new Morden chain spec. diff --git a/scripts/contractABI.js b/scripts/contractABI.js index 198055c12..4db0fe3a1 100644 --- a/scripts/contractABI.js +++ b/scripts/contractABI.js @@ -41,11 +41,11 @@ ${convertContract(name, json, prefs)} function convertContract(name, json, prefs) { return `${prefs._pub ? "pub " : ""}struct ${name} { contract: ethabi::Contract, - address: util::Address, + pub address: util::Address, ${prefs._explicit_do_call ? "" : `do_call: Box<Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send${prefs._sync ? " + Sync " : ""}+ 'static>,`} } impl ${name} { - pub fn new${prefs._explicit_do_call ? "" : "<F>"}(address: util::Address${prefs._explicit_do_call ? "" : `", do_call: F"`}) -> Self + pub fn new${prefs._explicit_do_call ? "" : "<F>"}(address: util::Address${prefs._explicit_do_call ? "" : `, do_call: F`}) -> Self ${prefs._explicit_do_call ? "" : `where F: Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send ${prefs._sync ? "+ Sync " : ""}+ 'static`} { ${name} { contract: ethabi::Contract::new(ethabi::Interface::load(b"${JSON.stringify(json.filter(a => a.type == 'function')).replaceAll('"', '\\"')}").expect("JSON is autogenerated; qed")), @@ -233,19 +233,23 @@ function convertFunction(json, _prefs) { let prefs = {"_pub": true, "_": {"_client": {"string": true}, "_platform": {"string": true}}, "_sync": true}; // default contract json ABI let jsonabi = [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}]; +// default name +let name = 'Contract'; // parse command line options for (let i = 1; i < process.argv.length; ++i) { let arg = process.argv[i]; - if (arg.indexOf("--jsonabi") == 0) { + if (arg.indexOf("--jsonabi=") == 0) { jsonabi = arg.slice(10); if (fs.existsSync(jsonabi)) { jsonabi = JSON.parse(fs.readFileSync(jsonabi).toString()); } } else if (arg.indexOf("--explicit-do-call") == 0) { prefs._explicit_do_call = true; + } else if (arg.indexOf("--name=") == 0) { + name = arg.slice(7); } } -let out = makeContractFile("Contract", jsonabi, prefs); +let out = makeContractFile(name, jsonabi, prefs); console.log(`${out}`); From 374d7c879e315d1f26d043ba3ff583c8f5be684b Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 7 Mar 2017 01:56:19 +0400 Subject: [PATCH 69/93] push-release after build --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8081e53c0..a11cbc2a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ stages: - test - js-build - - push-release - build + - push-release variables: GIT_DEPTH: "3" SIMPLECOV: "true" From 4e1816cd00c3ae919f3f8a0370bdbcb485c7f51b Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 7 Mar 2017 03:16:32 +0400 Subject: [PATCH 70/93] test coverage --- scripts/cov.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cov.sh b/scripts/cov.sh index 036cf9c80..7f21e9804 100755 --- a/scripts/cov.sh +++ b/scripts/cov.sh @@ -50,6 +50,6 @@ do $KCOV --exclude-pattern $EXCLUDE $KCOV_FLAGS $KCOV_TARGET $FILE done -$KCOV --coveralls-id=${TRAVIS_JOB_ID} --exclude-pattern $EXCLUDE $KCOV_FLAGS $KCOV_TARGET target/debug/parity-* +$KCOV --coveralls-id=${CI_BUILD_ID} --exclude-pattern $EXCLUDE $KCOV_FLAGS $KCOV_TARGET target/debug/parity-* exit 0 From 2f7f95d519acf75fc3cb6e6d6427432b1099d9e3 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 7 Mar 2017 03:21:11 +0400 Subject: [PATCH 71/93] update gitlab-ci add `kcov` cmd --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a11cbc2a6..305dccd9a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -537,7 +537,7 @@ test-rust-stable: - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v -e ^js -e ^\\. -e ^LICENSE -e ^README.md -e ^appveyor.yml -e ^test.sh -e ^windows/ -e ^scripts/ -e^mac/ -e ^nsis/ | wc -l) script: - export RUST_BACKTRACE=1 - - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi + - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS&&./scripts/cov.sh "$KCOV_CMD"; fi tags: - rust - rust-stable From 63bc942e4117a87e97ac2650e31c2ea631fef29b Mon Sep 17 00:00:00 2001 From: Igor Artamonov <igor@artamonov.ru> Date: Mon, 6 Mar 2017 23:34:48 -0500 Subject: [PATCH 72/93] update ETC bootnodes (#4794) --- ethcore/res/ethereum/classic.json | 14 ++++---------- ethcore/res/ethereum/morden.json | 5 ++++- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ethcore/res/ethereum/classic.json b/ethcore/res/ethereum/classic.json index b165fe169..33c954f2c 100644 --- a/ethcore/res/ethereum/classic.json +++ b/ethcore/res/ethereum/classic.json @@ -48,18 +48,12 @@ "stateRoot": "0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544" }, "nodes": [ - "enode://08c7ee6a4f861ff0664a49532bcc86de1363acd608999d1b76609bb9bc278649906f069057630fd9493924a368b5d1dc9b8f8bf13ac26df72512f6d1fabd8c95@45.32.7.81:30303", "enode://e809c4a2fec7daed400e5e28564e23693b23b2cc5a019b612505631bbe7b9ccf709c1796d2a3d29ef2b045f210caf51e3c4f5b6d3587d43ad5d6397526fa6179@174.112.32.157:30303", - "enode://687be94c3a7beaa3d2fde82fa5046cdeb3e8198354e05b29d6e0d4e276713e3707ac10f784a7904938b06b46c764875c241b0337dd853385a4d8bfcbf8190647@95.183.51.229:30303", "enode://6e538e7c1280f0a31ff08b382db5302480f775480b8e68f8febca0ceff81e4b19153c6f8bf60313b93bef2cc34d34e1df41317de0ce613a201d1660a788a03e2@52.206.67.235:30303", - "enode://ca5ae4eca09ba6787e29cf6d86f7634d07aae6b9e6317a59aff675851c0bf445068173208cf8ef7f5cd783d8e29b85b2fa3fa358124cf0546823149724f9bde1@138.68.1.16:30303", - "enode://217ebe27e89bf4fec8ce06509323ff095b1014378deb75ab2e5f6759a4e8750a3bd8254b8c6833136e4d5e58230d65ee8ab34a5db5abf0640408c4288af3c8a7@188.138.1.237:30303", - "enode://fa20444ef991596ce99b81652ac4e61de1eddc4ff21d3cd42762abd7ed47e7cf044d3c9ccddaf6035d39725e4eb372806787829ccb9a08ec7cb71883cb8c3abd@50.149.116.182:30303", - "enode://4bd6a4df3612c718333eb5ea7f817923a8cdf1bed89cee70d1710b45a0b6b77b2819846440555e451a9b602ad2efa2d2facd4620650249d8468008946887820a@71.178.232.20:30304", - "enode://921cf8e4c345fe8db913c53964f9cadc667644e7f20195a0b7d877bd689a5934e146ff2c2259f1bae6817b6585153a007ceb67d260b720fa3e6fc4350df25c7f@51.255.49.170:30303", - "enode://ffea3b01c000cdd89e1e9229fea3e80e95b646f9b2aa55071fc865e2f19543c9b06045cc2e69453e6b78100a119e66be1b5ad50b36f2ffd27293caa28efdd1b2@128.199.93.177:3030", - "enode://ee3da491ce6a155eb132708eb0e8d04b0637926ec0ae1b79e63fc97cb9fc3818f49250a0ae0d7f79ed62b66ec677f408c4e01741504dc7a051e274f1e803d454@91.121.65.179:40404", - "enode://48e063a6cf5f335b1ef2ed98126bf522cf254396f850c7d442fe2edbbc23398787e14cd4de7968a00175a82762de9cbe9e1407d8ccbcaeca5004d65f8398d759@159.203.255.59:30303" + "enode://5fbfb426fbb46f8b8c1bd3dd140f5b511da558cd37d60844b525909ab82e13a25ee722293c829e52cb65c2305b1637fa9a2ea4d6634a224d5f400bfe244ac0de@162.243.55.45:30303", + "enode://42d8f29d1db5f4b2947cd5c3d76c6d0d3697e6b9b3430c3d41e46b4bb77655433aeedc25d4b4ea9d8214b6a43008ba67199374a9b53633301bca0cd20c6928ab@104.155.176.151:30303", + "enode://814920f1ec9510aa9ea1c8f79d8b6e6a462045f09caa2ae4055b0f34f7416fca6facd3dd45f1cf1673c0209e0503f02776b8ff94020e98b6679a0dc561b4eba0@104.154.136.117:30303", + "enode://72e445f4e89c0f476d404bc40478b0df83a5b500d2d2e850e08eb1af0cd464ab86db6160d0fde64bd77d5f0d33507ae19035671b3c74fec126d6e28787669740@104.198.71.200:30303" ], "accounts": { "0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/res/ethereum/morden.json b/ethcore/res/ethereum/morden.json index 5b64b63da..22f253bf8 100644 --- a/ethcore/res/ethereum/morden.json +++ b/ethcore/res/ethereum/morden.json @@ -48,7 +48,10 @@ }, "nodes": [ "enode://e731347db0521f3476e6bbbb83375dcd7133a1601425ebd15fd10f3835fd4c304fba6282087ca5a0deeafadf0aa0d4fd56c3323331901c1f38bd181c283e3e35@128.199.55.137:30303", - "enode://ceb5c0f85eb994dbe9693bf46d99b03f6b838d17cc74e68d5eb003171ff39e5f120b17f965b267c319303f94d80b9d994b77062fb1486d76ce95d9f3d8fe1cb4@46.101.122.141:30303" + "enode://ceb5c0f85eb994dbe9693bf46d99b03f6b838d17cc74e68d5eb003171ff39e5f120b17f965b267c319303f94d80b9d994b77062fb1486d76ce95d9f3d8fe1cb4@46.101.122.141:30303", + "enode://fb28713820e718066a2f5df6250ae9d07cff22f672dbf26be6c75d088f821a9ad230138ba492c533a80407d054b1436ef18e951bb65e6901553516c8dffe8ff0@104.155.176.151:30304", + "enode://afdc6076b9bf3e7d3d01442d6841071e84c76c73a7016cb4f35c0437df219db38565766234448f1592a07ba5295a867f0ce87b359bf50311ed0b830a2361392d@104.154.136.117:30403", + "enode://21101a9597b79e933e17bc94ef3506fe99a137808907aa8fefa67eea4b789792ad11fb391f38b00087f8800a2d3dff011572b62a31232133dd1591ac2d1502c8@104.198.71.200:30403" ], "accounts": { "0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, From 3fb8466c403ce3738b33bf3eff2b4288403ecef4 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 7 Mar 2017 17:29:15 +0100 Subject: [PATCH 73/93] Bump package.json for master 1.7 (#4731) * Bump package.json for master 1.7 * Update contributors --- js/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/package.json b/js/package.json index d78d14139..55569372d 100644 --- a/js/package.json +++ b/js/package.json @@ -1,15 +1,16 @@ { "name": "parity.js", - "version": "0.3.120", + "version": "1.7.0", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", "maintainers": [ "Jaco Greeff", - "Nicolas Gotchac", + "Nicolas Gotchac" + ], + "contributors": [ "Jannis Redmann" ], - "contributors": [], "license": "GPL-3.0", "repository": { "type": "git", From ae3f85bd5b071e7484cce34513f929a0181ac9b1 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Tue, 7 Mar 2017 17:29:27 +0100 Subject: [PATCH 74/93] v1.7 (#4730) --- Cargo.lock | 270 ++++++++++++++++++------------------ dapps/Cargo.toml | 2 +- dapps/js-glue/Cargo.toml | 2 +- dapps/ui/Cargo.toml | 2 +- db/Cargo.toml | 2 +- devtools/Cargo.toml | 2 +- ethash/Cargo.toml | 2 +- ethcore/Cargo.toml | 2 +- ethcore/light/Cargo.toml | 2 +- evmjit/Cargo.toml | 2 +- hash-fetch/Cargo.toml | 2 +- hw/Cargo.toml | 2 +- ipc-common-types/Cargo.toml | 2 +- ipc/codegen/Cargo.toml | 2 +- ipc/nano/Cargo.toml | 2 +- ipc/rpc/Cargo.toml | 2 +- ipfs/Cargo.toml | 2 +- logger/Cargo.toml | 2 +- mac/Parity.pkgproj | 2 +- nsis/installer.nsi | 2 +- rpc/Cargo.toml | 2 +- rpc/rpctest/Cargo.toml | 2 +- signer/Cargo.toml | 2 +- stratum/Cargo.toml | 2 +- sync/Cargo.toml | 2 +- updater/Cargo.toml | 2 +- util/Cargo.toml | 2 +- util/io/Cargo.toml | 2 +- util/network/Cargo.toml | 2 +- 29 files changed, 163 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a6750355..1ad38ac89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,22 +9,22 @@ dependencies = [ "daemonize 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore 1.6.0", - "ethcore-dapps 1.6.0", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-ipc 1.6.0", + "ethcore 1.7.0", + "ethcore-dapps 1.7.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-ipc 1.7.0", "ethcore-ipc-hypervisor 1.2.0", - "ethcore-ipc-nano 1.6.0", + "ethcore-ipc-nano 1.7.0", "ethcore-ipc-tests 0.1.0", - "ethcore-light 1.6.0", - "ethcore-logger 1.6.0", - "ethcore-rpc 1.6.0", + "ethcore-light 1.7.0", + "ethcore-logger 1.7.0", + "ethcore-rpc 1.7.0", "ethcore-secretstore 1.0.0", - "ethcore-signer 1.6.0", - "ethcore-stratum 1.6.0", - "ethcore-util 1.6.0", - "ethsync 1.6.0", + "ethcore-signer 1.7.0", + "ethcore-stratum 1.7.0", + "ethcore-util 1.7.0", + "ethsync 1.7.0", "evmbin 0.1.0", "fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", @@ -33,12 +33,12 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-hash-fetch 1.6.0", - "parity-ipfs-api 1.6.0", + "parity-hash-fetch 1.7.0", + "parity-ipfs-api 1.7.0", "parity-local-store 0.1.0", "parity-reactor 0.1.0", "parity-rpc-client 1.4.0", - "parity-updater 1.6.0", + "parity-updater 1.7.0", "regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.1.0", "rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -367,7 +367,7 @@ dependencies = [ [[package]] name = "ethash" -version = "1.6.0" +version = "1.7.0" dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -377,7 +377,7 @@ dependencies = [ [[package]] name = "ethcore" -version = "1.6.0" +version = "1.7.0" dependencies = [ "bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "bloomchain 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -386,20 +386,20 @@ dependencies = [ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "ethabi 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ethash 1.6.0", + "ethash 1.7.0", "ethcore-bloom-journal 0.1.0", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", - "ethcore-stratum 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", + "ethcore-stratum 1.7.0", + "ethcore-util 1.7.0", "ethjson 0.1.0", "ethkey 0.2.0", "ethstore 0.1.0", - "evmjit 1.6.0", - "hardware-wallet 1.6.0", + "evmjit 1.7.0", + "hardware-wallet 1.7.0", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -436,14 +436,14 @@ dependencies = [ [[package]] name = "ethcore-dapps" -version = "1.6.0" +version = "1.7.0" dependencies = [ "base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.6.0", - "ethcore-rpc 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-rpc 1.7.0", + "ethcore-util 1.7.0", "fetch 0.1.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", @@ -454,9 +454,9 @@ dependencies = [ "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-hash-fetch 1.6.0", + "parity-hash-fetch 1.7.0", "parity-reactor 0.1.0", - "parity-ui 1.6.0", + "parity-ui 1.7.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -470,14 +470,14 @@ dependencies = [ [[package]] name = "ethcore-devtools" -version = "1.6.0" +version = "1.7.0" dependencies = [ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "ethcore-io" -version = "1.6.0" +version = "1.7.0" dependencies = [ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -488,17 +488,17 @@ dependencies = [ [[package]] name = "ethcore-ipc" -version = "1.6.0" +version = "1.7.0" dependencies = [ - "ethcore-devtools 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-util 1.7.0", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "ethcore-ipc-codegen" -version = "1.6.0" +version = "1.7.0" dependencies = [ "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -511,9 +511,9 @@ dependencies = [ name = "ethcore-ipc-hypervisor" version = "1.2.0" dependencies = [ - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -522,9 +522,9 @@ dependencies = [ [[package]] name = "ethcore-ipc-nano" -version = "1.6.0" +version = "1.7.0" dependencies = [ - "ethcore-ipc 1.6.0", + "ethcore-ipc 1.7.0", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", @@ -534,11 +534,11 @@ dependencies = [ name = "ethcore-ipc-tests" version = "0.1.0" dependencies = [ - "ethcore-devtools 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", + "ethcore-util 1.7.0", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -546,14 +546,14 @@ dependencies = [ [[package]] name = "ethcore-light" -version = "1.6.0" +version = "1.7.0" dependencies = [ - "ethcore 1.6.0", - "ethcore-io 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-network 1.6.0", - "ethcore-util 1.6.0", + "ethcore 1.7.0", + "ethcore-io 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-network 1.7.0", + "ethcore-util 1.7.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -566,10 +566,10 @@ dependencies = [ [[package]] name = "ethcore-logger" -version = "1.6.0" +version = "1.7.0" dependencies = [ "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-util 1.6.0", + "ethcore-util 1.7.0", "isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -579,13 +579,13 @@ dependencies = [ [[package]] name = "ethcore-network" -version = "1.6.0" +version = "1.7.0" dependencies = [ "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-util 1.7.0", "ethcrypto 0.1.0", "ethkey 0.2.0", "igd 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -604,21 +604,21 @@ dependencies = [ [[package]] name = "ethcore-rpc" -version = "1.6.0" +version = "1.7.0" dependencies = [ "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", - "ethash 1.6.0", - "ethcore 1.6.0", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-light 1.6.0", - "ethcore-util 1.6.0", + "ethash 1.7.0", + "ethcore 1.7.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-light 1.7.0", + "ethcore-util 1.7.0", "ethcrypto 0.1.0", "ethjson 0.1.0", "ethkey 0.2.0", "ethstore 0.1.0", - "ethsync 1.6.0", + "ethsync 1.7.0", "fetch 0.1.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", @@ -628,7 +628,7 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "parity-reactor 0.1.0", - "parity-updater 1.6.0", + "parity-updater 1.7.0", "rlp 0.1.0", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -644,11 +644,11 @@ dependencies = [ name = "ethcore-secretstore" version = "1.0.0" dependencies = [ - "ethcore-devtools 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", + "ethcore-util 1.7.0", "ethcrypto 0.1.0", "ethkey 0.2.0", "hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -659,18 +659,18 @@ dependencies = [ [[package]] name = "ethcore-signer" -version = "1.6.0" +version = "1.7.0" dependencies = [ "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-rpc 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-rpc 1.7.0", + "ethcore-util 1.7.0", "jsonrpc-core 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-ui 1.6.0", + "parity-ui 1.7.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)", @@ -678,14 +678,14 @@ dependencies = [ [[package]] name = "ethcore-stratum" -version = "1.6.0" +version = "1.7.0" dependencies = [ "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", + "ethcore-util 1.7.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-macros 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "ethcore-util" -version = "1.6.0" +version = "1.7.0" dependencies = [ "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", @@ -709,7 +709,7 @@ dependencies = [ "eth-secp256k1 0.5.6 (git+https://github.com/ethcore/rust-secp256k1)", "ethcore-bigint 0.1.2", "ethcore-bloom-journal 0.1.0", - "ethcore-devtools 1.6.0", + "ethcore-devtools 1.7.0", "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -748,7 +748,7 @@ dependencies = [ name = "ethjson" version = "0.1.0" dependencies = [ - "ethcore-util 1.6.0", + "ethcore-util 1.7.0", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -775,8 +775,8 @@ name = "ethstore" version = "0.1.0" dependencies = [ "docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.6.0", - "ethcore-util 1.6.0", + "ethcore-devtools 1.7.0", + "ethcore-util 1.7.0", "ethcrypto 0.1.0", "ethkey 0.2.0", "itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -797,19 +797,19 @@ dependencies = [ [[package]] name = "ethsync" -version = "1.6.0" +version = "1.7.0" dependencies = [ "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore 1.6.0", - "ethcore-devtools 1.6.0", - "ethcore-io 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-ipc-nano 1.6.0", - "ethcore-light 1.6.0", - "ethcore-network 1.6.0", - "ethcore-util 1.6.0", + "ethcore 1.7.0", + "ethcore-devtools 1.7.0", + "ethcore-io 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-ipc-nano 1.7.0", + "ethcore-light 1.7.0", + "ethcore-network 1.7.0", + "ethcore-util 1.7.0", "ethkey 0.2.0", "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -825,14 +825,14 @@ name = "evmbin" version = "0.1.0" dependencies = [ "docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore 1.6.0", - "ethcore-util 1.6.0", + "ethcore 1.7.0", + "ethcore-util 1.7.0", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "evmjit" -version = "1.6.0" +version = "1.7.0" dependencies = [ "tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -913,7 +913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "hardware-wallet" -version = "1.6.0" +version = "1.7.0" dependencies = [ "ethcore-bigint 0.1.2", "ethkey 0.2.0", @@ -1050,11 +1050,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "ipc-common-types" -version = "1.6.0" +version = "1.7.0" dependencies = [ - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-util 1.6.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-util 1.7.0", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1618,10 +1618,10 @@ dependencies = [ [[package]] name = "parity-hash-fetch" -version = "1.6.0" +version = "1.7.0" dependencies = [ "ethabi 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-util 1.6.0", + "ethcore-util 1.7.0", "fetch 0.1.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1634,11 +1634,11 @@ dependencies = [ [[package]] name = "parity-ipfs-api" -version = "1.6.0" +version = "1.7.0" dependencies = [ "cid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore 1.6.0", - "ethcore-util 1.6.0", + "ethcore 1.7.0", + "ethcore-util 1.7.0", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", "jsonrpc-http-server 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1650,9 +1650,9 @@ dependencies = [ name = "parity-local-store" version = "0.1.0" dependencies = [ - "ethcore 1.6.0", - "ethcore-io 1.6.0", - "ethcore-util 1.6.0", + "ethcore 1.7.0", + "ethcore-io 1.7.0", + "ethcore-util 1.7.0", "ethkey 0.2.0", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.1.0", @@ -1673,9 +1673,9 @@ dependencies = [ name = "parity-rpc-client" version = "1.4.0" dependencies = [ - "ethcore-rpc 1.6.0", - "ethcore-signer 1.6.0", - "ethcore-util 1.6.0", + "ethcore-rpc 1.7.0", + "ethcore-signer 1.7.0", + "ethcore-util 1.7.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1689,7 +1689,7 @@ dependencies = [ [[package]] name = "parity-ui" -version = "1.6.0" +version = "1.7.0" dependencies = [ "parity-ui-dev 1.4.0", "parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)", @@ -1713,17 +1713,17 @@ dependencies = [ [[package]] name = "parity-updater" -version = "1.6.0" +version = "1.7.0" dependencies = [ "ethabi 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore 1.6.0", - "ethcore-ipc 1.6.0", - "ethcore-ipc-codegen 1.6.0", - "ethcore-util 1.6.0", - "ethsync 1.6.0", - "ipc-common-types 1.6.0", + "ethcore 1.7.0", + "ethcore-ipc 1.7.0", + "ethcore-ipc-codegen 1.7.0", + "ethcore-util 1.7.0", + "ethsync 1.7.0", + "ipc-common-types 1.7.0", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-hash-fetch 1.6.0", + "parity-hash-fetch 1.7.0", "parity-reactor 0.1.0", "target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1992,8 +1992,8 @@ name = "rpc-cli" version = "1.4.0" dependencies = [ "ethcore-bigint 0.1.2", - "ethcore-rpc 1.6.0", - "ethcore-util 1.6.0", + "ethcore-rpc 1.7.0", + "ethcore-util 1.7.0", "futures 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "parity-rpc-client 1.4.0", "rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml index 2c99dde4f..508fbc1a0 100644 --- a/dapps/Cargo.toml +++ b/dapps/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Parity Dapps crate" name = "ethcore-dapps" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] diff --git a/dapps/js-glue/Cargo.toml b/dapps/js-glue/Cargo.toml index 1074330be..b53b158c7 100644 --- a/dapps/js-glue/Cargo.toml +++ b/dapps/js-glue/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Base Package for all Parity built-in dapps" name = "parity-dapps-glue" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/dapps/ui/Cargo.toml b/dapps/ui/Cargo.toml index e17948204..2ab128ad5 100644 --- a/dapps/ui/Cargo.toml +++ b/dapps/ui/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore Parity UI" homepage = "http://parity.io" license = "GPL-3.0" name = "parity-ui" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [build-dependencies] diff --git a/db/Cargo.toml b/db/Cargo.toml index fcceaa17d..a3fe0804c 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore Database" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-db" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 0cf5a6b2e..8759b81c9 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore development/test/build tools" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-devtools" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [dependencies] diff --git a/ethash/Cargo.toml b/ethash/Cargo.toml index 36909a525..8be24f9ae 100644 --- a/ethash/Cargo.toml +++ b/ethash/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethash" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [lib] diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index c8a1c7fb5..64010fadf 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore library" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/ethcore/light/Cargo.toml b/ethcore/light/Cargo.toml index 9e10449fb..d8844dc3f 100644 --- a/ethcore/light/Cargo.toml +++ b/ethcore/light/Cargo.toml @@ -3,7 +3,7 @@ description = "Parity LES primitives" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-light" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/evmjit/Cargo.toml b/evmjit/Cargo.toml index 0d5d16ea1..2f84a7efd 100644 --- a/evmjit/Cargo.toml +++ b/evmjit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evmjit" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [lib] diff --git a/hash-fetch/Cargo.toml b/hash-fetch/Cargo.toml index f5f31c0a0..d24315eb0 100644 --- a/hash-fetch/Cargo.toml +++ b/hash-fetch/Cargo.toml @@ -3,7 +3,7 @@ description = "Fetching hash-addressed content." homepage = "http://parity.io" license = "GPL-3.0" name = "parity-hash-fetch" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [dependencies] diff --git a/hw/Cargo.toml b/hw/Cargo.toml index 39baa675d..19b680a55 100644 --- a/hw/Cargo.toml +++ b/hw/Cargo.toml @@ -3,7 +3,7 @@ description = "Hardware wallet support." homepage = "http://parity.io" license = "GPL-3.0" name = "hardware-wallet" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [dependencies] diff --git a/ipc-common-types/Cargo.toml b/ipc-common-types/Cargo.toml index 844962b2d..1eb4a4d0a 100644 --- a/ipc-common-types/Cargo.toml +++ b/ipc-common-types/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Types that implement IPC and are common to multiple modules." name = "ipc-common-types" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/ipc/codegen/Cargo.toml b/ipc/codegen/Cargo.toml index a257fd98c..63e0fc7c6 100644 --- a/ipc/codegen/Cargo.toml +++ b/ipc/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethcore-ipc-codegen" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] license = "GPL-3.0" description = "Macros to auto-generate implementations for ipc call" diff --git a/ipc/nano/Cargo.toml b/ipc/nano/Cargo.toml index 07ebf41ef..9948820fe 100644 --- a/ipc/nano/Cargo.toml +++ b/ipc/nano/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethcore-ipc-nano" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] license = "GPL-3.0" diff --git a/ipc/rpc/Cargo.toml b/ipc/rpc/Cargo.toml index fdfc4ba80..d8be8b444 100644 --- a/ipc/rpc/Cargo.toml +++ b/ipc/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethcore-ipc" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] license = "GPL-3.0" diff --git a/ipfs/Cargo.toml b/ipfs/Cargo.toml index 992ee0710..b7b84b66b 100644 --- a/ipfs/Cargo.toml +++ b/ipfs/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Parity IPFS-compatible API" name = "parity-ipfs-api" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] diff --git a/logger/Cargo.toml b/logger/Cargo.toml index 091f9fefa..e0004cd48 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Ethcore client." name = "ethcore-logger" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] diff --git a/mac/Parity.pkgproj b/mac/Parity.pkgproj index caecc7c0d..cc7810dba 100755 --- a/mac/Parity.pkgproj +++ b/mac/Parity.pkgproj @@ -462,7 +462,7 @@ <key>OVERWRITE_PERMISSIONS</key> <false/> <key>VERSION</key> - <string>1.6.0</string> + <string>1.7.0</string> </dict> <key>UUID</key> <string>2DCD5B81-7BAF-4DA1-9251-6274B089FD36</string> diff --git a/nsis/installer.nsi b/nsis/installer.nsi index f40765944..cc4a0243b 100644 --- a/nsis/installer.nsi +++ b/nsis/installer.nsi @@ -9,7 +9,7 @@ !define COMPANYNAME "Ethcore" !define DESCRIPTION "Fast, light, robust Ethereum implementation" !define VERSIONMAJOR 1 -!define VERSIONMINOR 6 +!define VERSIONMINOR 7 !define VERSIONBUILD 0 !define ARGS "--warp" !define FIRST_START_ARGS "ui --warp --mode=passive" diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 91058b990..13ce8962f 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Ethcore jsonrpc" name = "ethcore-rpc" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] diff --git a/rpc/rpctest/Cargo.toml b/rpc/rpctest/Cargo.toml index 5951ef380..1d37ee264 100644 --- a/rpc/rpctest/Cargo.toml +++ b/rpc/rpctest/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Rpc test client." name = "rpctest" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 5a24c52bb..ba33bad68 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore Trusted Signer" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-signer" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/stratum/Cargo.toml b/stratum/Cargo.toml index 65b2c0e9c..75a45eacc 100644 --- a/stratum/Cargo.toml +++ b/stratum/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Ethcore stratum lib" name = "ethcore-stratum" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/sync/Cargo.toml b/sync/Cargo.toml index 5fa635760..c3eabbb02 100644 --- a/sync/Cargo.toml +++ b/sync/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Ethcore blockchain sync" name = "ethsync" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/updater/Cargo.toml b/updater/Cargo.toml index 8d0014e14..c0738cc59 100644 --- a/updater/Cargo.toml +++ b/updater/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Parity Updater Service." name = "parity-updater" -version = "1.6.0" +version = "1.7.0" license = "GPL-3.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/util/Cargo.toml b/util/Cargo.toml index 7a195212a..d32d2519a 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore utility library" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-util" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] build = "build.rs" diff --git a/util/io/Cargo.toml b/util/io/Cargo.toml index d433f66f2..d81557f8e 100644 --- a/util/io/Cargo.toml +++ b/util/io/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore IO library" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-io" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [dependencies] diff --git a/util/network/Cargo.toml b/util/network/Cargo.toml index 4b256bf1d..7fced467a 100644 --- a/util/network/Cargo.toml +++ b/util/network/Cargo.toml @@ -3,7 +3,7 @@ description = "Ethcore network library" homepage = "http://parity.io" license = "GPL-3.0" name = "ethcore-network" -version = "1.6.0" +version = "1.7.0" authors = ["Parity Technologies <admin@parity.io>"] [dependencies] From 4868f758bfde547108d31071ef4d8dffedaf5060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Tue, 7 Mar 2017 17:33:28 +0100 Subject: [PATCH 75/93] Allow specifying extra cors headers for dapps (#4710) --- dapps/src/api/api.rs | 6 +++++- dapps/src/lib.rs | 32 +++++++++++++++++++++++++------- dapps/src/tests/api.rs | 24 +++++++++++++++++++++++- dapps/src/tests/helpers/mod.rs | 4 ++++ parity/cli/mod.rs | 5 +++++ parity/cli/usage.txt | 2 ++ parity/configuration.rs | 5 +++++ parity/dapps.rs | 8 +++++++- 8 files changed, 76 insertions(+), 10 deletions(-) diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs index b521c0ba1..9106e0d70 100644 --- a/dapps/src/api/api.rs +++ b/dapps/src/api/api.rs @@ -39,7 +39,11 @@ pub struct RestApi { impl RestApi { pub fn new(cors_domains: Vec<String>, endpoints: Arc<Endpoints>, fetcher: Arc<Fetcher>) -> Box<Endpoint> { Box::new(RestApi { - cors_domains: Some(cors_domains.into_iter().map(AccessControlAllowOrigin::Value).collect()), + cors_domains: Some(cors_domains.into_iter().map(|domain| match domain.as_ref() { + "all" | "*" | "any" => AccessControlAllowOrigin::Any, + "null" => AccessControlAllowOrigin::Null, + other => AccessControlAllowOrigin::Value(other.into()), + }).collect()), endpoints: endpoints, fetcher: fetcher, }) diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 3f26e82a9..30c62a031 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -111,6 +111,7 @@ pub struct ServerBuilder<T: Fetch = FetchClient> { web_proxy_tokens: Arc<WebProxyTokens>, signer_address: Option<(String, u16)>, allowed_hosts: Option<Vec<String>>, + extra_cors: Option<Vec<String>>, remote: Remote, fetch: Option<T>, } @@ -126,6 +127,7 @@ impl ServerBuilder { web_proxy_tokens: Arc::new(|_| false), signer_address: None, allowed_hosts: Some(vec![]), + extra_cors: None, remote: remote, fetch: None, } @@ -143,6 +145,7 @@ impl<T: Fetch> ServerBuilder<T> { web_proxy_tokens: self.web_proxy_tokens, signer_address: self.signer_address, allowed_hosts: self.allowed_hosts, + extra_cors: self.extra_cors, remote: self.remote, fetch: Some(fetch), } @@ -174,6 +177,13 @@ impl<T: Fetch> ServerBuilder<T> { self } + /// Extra cors headers. + /// `None` - no additional CORS URLs + pub fn extra_cors_headers(mut self, cors: Option<Vec<String>>) -> Self { + self.extra_cors = cors; + self + } + /// Change extra dapps paths (apart from `dapps_path`) pub fn extra_dapps<P: AsRef<Path>>(mut self, extra_dapps: &[P]) -> Self { self.extra_dapps = extra_dapps.iter().map(|p| p.as_ref().to_owned()).collect(); @@ -187,6 +197,7 @@ impl<T: Fetch> ServerBuilder<T> { Server::start_http( addr, self.allowed_hosts, + self.extra_cors, NoAuth, handler, self.dapps_path, @@ -207,6 +218,7 @@ impl<T: Fetch> ServerBuilder<T> { Server::start_http( addr, self.allowed_hosts, + self.extra_cors, HttpBasicAuth::single_user(username, password), handler, self.dapps_path, @@ -251,8 +263,8 @@ impl Server { } /// Returns a list of CORS domains for API endpoint. - fn cors_domains(signer_address: Option<(String, u16)>) -> Vec<String> { - match signer_address { + fn cors_domains(signer_address: Option<(String, u16)>, extra_cors: Option<Vec<String>>) -> Vec<String> { + let basic_cors = match signer_address { Some(signer_address) => vec![ format!("http://{}{}", HOME_PAGE, DAPPS_DOMAIN), format!("http://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1), @@ -260,15 +272,20 @@ impl Server { format!("https://{}{}", HOME_PAGE, DAPPS_DOMAIN), format!("https://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1), format!("https://{}", address(&signer_address)), - ], None => vec![], + }; + + match extra_cors { + None => basic_cors, + Some(extra_cors) => basic_cors.into_iter().chain(extra_cors).collect(), } } fn start_http<A: Authorization + 'static, F: Fetch, T: Middleware<Metadata>>( addr: &SocketAddr, hosts: Option<Vec<String>>, + extra_cors: Option<Vec<String>>, authorization: A, handler: RpcHandler<Metadata, T>, dapps_path: PathBuf, @@ -297,7 +314,7 @@ impl Server { remote.clone(), fetch.clone(), )); - let cors_domains = Self::cors_domains(signer_address.clone()); + let cors_domains = Self::cors_domains(signer_address.clone(), extra_cors); let special = Arc::new({ let mut special = HashMap::new(); @@ -413,8 +430,9 @@ mod util_tests { // given // when - let none = Server::cors_domains(None); - let some = Server::cors_domains(Some(("127.0.0.1".into(), 18180))); + let none = Server::cors_domains(None, None); + let some = Server::cors_domains(Some(("127.0.0.1".into(), 18180)), None); + let extra = Server::cors_domains(None, Some(vec!["all".to_owned()])); // then assert_eq!(none, Vec::<String>::new()); @@ -425,7 +443,7 @@ mod util_tests { "https://parity.web3.site".into(), "https://parity.web3.site:18180".into(), "https://127.0.0.1:18180".into() - ]); + assert_eq!(extra, vec!["all".to_owned()]); } } diff --git a/dapps/src/tests/api.rs b/dapps/src/tests/api.rs index 0930aa0ce..1b9f64b7f 100644 --- a/dapps/src/tests/api.rs +++ b/dapps/src/tests/api.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -use tests::helpers::{serve, serve_with_registrar, request, assert_security_headers}; +use tests::helpers::{serve, serve_with_registrar, serve_extra_cors, request, assert_security_headers}; #[test] fn should_return_error() { @@ -212,3 +212,25 @@ fn should_return_signer_port_cors_headers_for_home_parity_with_port() { ); } +#[test] +fn should_return_extra_cors_headers() { + // given + let server = serve_extra_cors(Some(vec!["all".to_owned()])); + + // when + let response = request(server, + "\ + POST /api/ping HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Origin: http://somedomain.io\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + response.assert_status("HTTP/1.1 200 OK"); + response.assert_header("Access-Control-Allow-Origin", "http://somedomain.io"); +} + diff --git a/dapps/src/tests/helpers/mod.rs b/dapps/src/tests/helpers/mod.rs index 9df98c343..d1a1e9900 100644 --- a/dapps/src/tests/helpers/mod.rs +++ b/dapps/src/tests/helpers/mod.rs @@ -109,6 +109,10 @@ pub fn serve_hosts(hosts: Option<Vec<String>>) -> ServerLoop { init_server(|builder| builder.allowed_hosts(hosts), Default::default(), Remote::new_sync()).0 } +pub fn serve_extra_cors(extra_cors: Option<Vec<String>>) -> ServerLoop { + init_server(|builder| builder.allowed_hosts(None).extra_cors_headers(extra_cors), Default::default(), Remote::new_sync()).0 +} + pub fn serve_with_registrar() -> (ServerLoop, Arc<FakeRegistrar>) { init_server(|builder| builder.allowed_hosts(None), Default::default(), Remote::new_sync()) } diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 5f062d425..cb256f0b7 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -181,6 +181,8 @@ usage! { or |c: &Config| otry!(c.dapps).interface.clone(), flag_dapps_hosts: String = "none", or |c: &Config| otry!(c.dapps).hosts.as_ref().map(|vec| vec.join(",")), + flag_dapps_cors: Option<String> = None, + or |c: &Config| otry!(c.dapps).cors.clone().map(Some), flag_dapps_path: String = "$BASE/dapps", or |c: &Config| otry!(c.dapps).path.clone(), flag_dapps_user: Option<String> = None, @@ -428,6 +430,7 @@ struct Dapps { port: Option<u16>, interface: Option<String>, hosts: Option<Vec<String>>, + cors: Option<String>, path: Option<String>, user: Option<String>, pass: Option<String>, @@ -674,6 +677,7 @@ mod tests { flag_dapps_port: 8080u16, flag_dapps_interface: "local".into(), flag_dapps_hosts: "none".into(), + flag_dapps_cors: None, flag_dapps_path: "$HOME/.parity/dapps".into(), flag_dapps_user: Some("test_user".into()), flag_dapps_pass: Some("test_pass".into()), @@ -873,6 +877,7 @@ mod tests { path: None, interface: None, hosts: None, + cors: None, user: Some("username".into()), pass: Some("password".into()) }), diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index a36d0a68c..da7a72db9 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -164,6 +164,8 @@ API and Console Options: is additional security against some attack vectors. Special options: "all", "none", (default: {flag_dapps_hosts}). + --dapps-cors URL Specify CORS headers for Dapps server APIs. + (default: {flag_dapps_cors:?}) --dapps-user USERNAME Specify username for Dapps server. It will be used in HTTP Basic Authentication Scheme. If --dapps-pass is not specified you will be diff --git a/parity/configuration.rs b/parity/configuration.rs index b50816482..c0756a771 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -546,6 +546,7 @@ impl Configuration { interface: self.dapps_interface(), port: self.args.flag_dapps_port, hosts: self.dapps_hosts(), + cors: self.dapps_cors(), user: self.args.flag_dapps_user.clone(), pass: self.args.flag_dapps_pass.clone(), dapps_path: PathBuf::from(self.directories().dapps), @@ -722,6 +723,10 @@ impl Configuration { Self::cors(self.args.flag_ipfs_api_cors.as_ref()) } + fn dapps_cors(&self) -> Option<Vec<String>> { + Self::cors(self.args.flag_dapps_cors.as_ref()) + } + fn hosts(hosts: &str) -> Option<Vec<String>> { match hosts { "none" => return Some(Vec::new()), diff --git a/parity/dapps.rs b/parity/dapps.rs index 572d32b48..b9094c16d 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -33,6 +33,7 @@ pub struct Configuration { pub interface: String, pub port: u16, pub hosts: Option<Vec<String>>, + pub cors: Option<Vec<String>>, pub user: Option<String>, pub pass: Option<String>, pub dapps_path: PathBuf, @@ -48,6 +49,7 @@ impl Default for Configuration { interface: "127.0.0.1".into(), port: 8080, hosts: Some(Vec::new()), + cors: None, user: None, pass: None, dapps_path: replace_home(&data_dir, "$BASE/dapps").into(), @@ -93,6 +95,7 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<We configuration.extra_dapps, &addr, configuration.hosts, + configuration.cors, auth, configuration.all_apis, )?)) @@ -114,6 +117,7 @@ mod server { _extra_dapps: Vec<PathBuf>, _url: &SocketAddr, _allowed_hosts: Option<Vec<String>>, + _cors: Option<Vec<String>>, _auth: Option<(String, String)>, _all_apis: bool, ) -> Result<WebappServer, String> { @@ -147,6 +151,7 @@ mod server { extra_dapps: Vec<PathBuf>, url: &SocketAddr, allowed_hosts: Option<Vec<String>>, + cors: Option<Vec<String>>, auth: Option<(String, String)>, all_apis: bool, ) -> Result<WebappServer, String> { @@ -167,7 +172,8 @@ mod server { .web_proxy_tokens(Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token))) .extra_dapps(&extra_dapps) .signer_address(deps.signer.address()) - .allowed_hosts(allowed_hosts); + .allowed_hosts(allowed_hosts) + .extra_cors_headers(cors); let api_set = if all_apis { warn!("{}", Colour::Red.bold().paint("*** INSECURE *** Running Dapps with all APIs exposed.")); From 973bb63dca8472880705d4be8a0e7a50a497d915 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 7 Mar 2017 22:33:08 +0400 Subject: [PATCH 76/93] update gitlab-ci remove temp kcov cmd [ci skip] --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 305dccd9a..a11cbc2a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -537,7 +537,7 @@ test-rust-stable: - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v -e ^js -e ^\\. -e ^LICENSE -e ^README.md -e ^appveyor.yml -e ^test.sh -e ^windows/ -e ^scripts/ -e^mac/ -e ^nsis/ | wc -l) script: - export RUST_BACKTRACE=1 - - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS&&./scripts/cov.sh "$KCOV_CMD"; fi + - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi tags: - rust - rust-stable From 4d08e7b0aec46443bf26547b17d10cb302672835 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Tue, 7 Mar 2017 20:19:55 +0100 Subject: [PATCH 77/93] Update Wallet to new Wallet Code (#4805) * Update Wallet Version * Update Wallet Library * Update Wallets Bytecodes * Typo * Separate Deploy in Contract API * Use the new Wallet ABI // Update wallet code * WIP .// Deploy from Wallet * Update Wallet contract * Contract Deployment for Wallet * Working deployments for Single Owned Wallet contracts * Linting * Create a Wallet from a Wallet * Linting * Fix Signer transactions // Add Gas Used for transactions * Deploy wallet contract fix * Fix too high gas estimate for Wallet Contract Deploys * Final piece ; deploying from Wallet owned by wallet * Update Wallet Code * Updated the Wallet Codes * Fixing Wallet Deployments * Add Support for older wallets * Linting --- js/src/api/contract/contract.js | 48 +- js/src/contracts/abi/old-wallet.json | 466 ++++++++++ js/src/contracts/abi/wallet.json | 477 +++++++++- js/src/contracts/code/wallet.js | 13 +- js/src/contracts/snippets/enhanced-wallet.sol | 815 +++++++++--------- js/src/contracts/snippets/wallet.sol | 665 +++++++------- .../WalletDetails/walletDetails.js | 6 +- .../modals/CreateWallet/createWalletStore.js | 89 +- .../modals/DeployContract/deployContract.js | 63 +- .../modals/WalletSettings/walletSettings.js | 5 + js/src/redux/providers/personal.js | 2 +- js/src/redux/providers/personalActions.js | 49 +- js/src/redux/providers/walletActions.js | 39 +- js/src/ui/MethodDecoding/methodDecoding.js | 12 + js/src/ui/TxList/store.js | 52 +- js/src/ui/TxList/txList.js | 4 +- js/src/util/tx.js | 119 ++- js/src/util/wallets.js | 208 ++++- js/src/views/Signer/store.js | 5 +- .../views/Wallet/Transactions/transactions.js | 7 +- 20 files changed, 2227 insertions(+), 917 deletions(-) create mode 100644 js/src/contracts/abi/old-wallet.json diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 570c36287..dd36afead 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -107,34 +107,26 @@ export default class Contract { }); } - deploy (options, values, statecb) { - const setState = (state) => { - if (!statecb) { - return; - } - - return statecb(null, state); - }; - - setState({ state: 'estimateGas' }); + deploy (options, values, statecb = () => {}) { + statecb(null, { state: 'estimateGas' }); return this .deployEstimateGas(options, values) .then(([gasEst, gas]) => { options.gas = gas.toFixed(0); - setState({ state: 'postTransaction', gas }); + statecb(null, { state: 'postTransaction', gas }); - const _options = this._encodeOptions(this.constructors[0], options, values); + const encodedOptions = this._encodeOptions(this.constructors[0], options, values); return this._api.parity - .postTransaction(_options) + .postTransaction(encodedOptions) .then((requestId) => { - setState({ state: 'checkRequest', requestId }); + statecb(null, { state: 'checkRequest', requestId }); return this._pollCheckRequest(requestId); }) .then((txhash) => { - setState({ state: 'getTransactionReceipt', txhash }); + statecb(null, { state: 'getTransactionReceipt', txhash }); return this._pollTransactionReceipt(txhash, gas); }) .then((receipt) => { @@ -142,23 +134,23 @@ export default class Contract { throw new Error(`Contract not deployed, gasUsed == ${gas.toFixed(0)}`); } - setState({ state: 'hasReceipt', receipt }); + statecb(null, { state: 'hasReceipt', receipt }); this._receipt = receipt; this._address = receipt.contractAddress; return this._address; - }); - }) - .then((address) => { - setState({ state: 'getCode' }); - return this._api.eth.getCode(this._address); - }) - .then((code) => { - if (code === '0x') { - throw new Error('Contract not deployed, getCode returned 0x'); - } + }) + .then((address) => { + statecb(null, { state: 'getCode' }); + return this._api.eth.getCode(this._address); + }) + .then((code) => { + if (code === '0x') { + throw new Error('Contract not deployed, getCode returned 0x'); + } - setState({ state: 'completed' }); - return this._address; + statecb(null, { state: 'completed' }); + return this._address; + }); }); } diff --git a/js/src/contracts/abi/old-wallet.json b/js/src/contracts/abi/old-wallet.json new file mode 100644 index 000000000..930069742 --- /dev/null +++ b/js/src/contracts/abi/old-wallet.json @@ -0,0 +1,466 @@ +[ + { + "constant": false, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "removeOwner", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_addr", + "type": "address" + } + ], + "name": "isOwner", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_numOwners", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_lastDay", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "resetSpentToday", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_spentToday", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "addOwner", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_required", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_h", + "type": "bytes32" + } + ], + "name": "confirm", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newLimit", + "type": "uint256" + } + ], + "name": "setDailyLimit", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_data", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [ + { + "name": "_r", + "type": "bytes32" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_operation", + "type": "bytes32" + } + ], + "name": "revoke", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newRequired", + "type": "uint256" + } + ], + "name": "changeRequirement", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_operation", + "type": "bytes32" + }, + { + "name": "_owner", + "type": "address" + } + ], + "name": "hasConfirmed", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "ownerIndex", + "type": "uint256" + } + ], + "name": "getOwner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + } + ], + "name": "kill", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + } + ], + "name": "changeOwner", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_dailyLimit", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "inputs": [ + { + "name": "_owners", + "type": "address[]" + }, + { + "name": "_required", + "type": "uint256" + }, + { + "name": "_daylimit", + "type": "uint256" + } + ], + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + } + ], + "name": "Confirmation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + } + ], + "name": "Revoke", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "oldOwner", + "type": "address" + }, + { + "indexed": false, + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "oldOwner", + "type": "address" + } + ], + "name": "OwnerRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "newRequirement", + "type": "uint256" + } + ], + "name": "RequirementChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_from", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + } + ], + "name": "SingleTransact", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + } + ], + "name": "MultiTransact", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "operation", + "type": "bytes32" + }, + { + "indexed": false, + "name": "initiator", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + } + ], + "name": "ConfirmationNeeded", + "type": "event" + } +] diff --git a/js/src/contracts/abi/wallet.json b/js/src/contracts/abi/wallet.json index 8048d239c..752407e62 100644 --- a/js/src/contracts/abi/wallet.json +++ b/js/src/contracts/abi/wallet.json @@ -1 +1,476 @@ -[{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"removeOwner","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_numOwners","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_lastDay","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"resetSpentToday","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_spentToday","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"addOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_required","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_h","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_newLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"_r","type":"bytes32"}],"type":"function"},{"constant":false,"inputs":[{"name":"_operation","type":"bytes32"}],"name":"revoke","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_newRequired","type":"uint256"}],"name":"changeRequirement","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_operation","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"hasConfirmed","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"ownerIndex","type":"uint256"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"}],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"changeOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_dailyLimit","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"},{"name":"_daylimit","type":"uint256"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Confirmation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Revoke","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"},{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"}],"name":"OwnerRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newRequirement","type":"uint256"}],"name":"RequirementChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"SingleTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"MultiTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"initiator","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"ConfirmationNeeded","type":"event"}] +[ + { + "constant": false, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "removeOwner", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_addr", + "type": "address" + } + ], + "name": "isOwner", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_numOwners", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_lastDay", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "resetSpentToday", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_spentToday", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "addOwner", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_required", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_h", + "type": "bytes32" + } + ], + "name": "confirm", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newLimit", + "type": "uint256" + } + ], + "name": "setDailyLimit", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_data", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [ + { + "name": "_r", + "type": "bytes32" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_operation", + "type": "bytes32" + } + ], + "name": "revoke", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_newRequired", + "type": "uint256" + } + ], + "name": "changeRequirement", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_operation", + "type": "bytes32" + }, + { + "name": "_owner", + "type": "address" + } + ], + "name": "hasConfirmed", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "ownerIndex", + "type": "uint256" + } + ], + "name": "getOwner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + } + ], + "name": "kill", + "outputs": [], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + } + ], + "name": "changeOwner", + "outputs": [], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "m_dailyLimit", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "inputs": [ + { + "name": "_owners", + "type": "address[]" + }, + { + "name": "_required", + "type": "uint256" + }, + { + "name": "_daylimit", + "type": "uint256" + } + ], + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + } + ], + "name": "Confirmation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + } + ], + "name": "Revoke", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "oldOwner", + "type": "address" + }, + { + "indexed": false, + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "oldOwner", + "type": "address" + } + ], + "name": "OwnerRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "newRequirement", + "type": "uint256" + } + ], + "name": "RequirementChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_from", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "name": "created", + "type": "address" + } + ], + "name": "SingleTransact", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "name": "operation", + "type": "bytes32" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "name": "created", + "type": "address" + } + ], + "name": "MultiTransact", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "operation", + "type": "bytes32" + }, + { + "indexed": false, + "name": "initiator", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "data", + "type": "bytes" + } + ], + "name": "ConfirmationNeeded", + "type": "event" + } +] diff --git a/js/src/contracts/code/wallet.js b/js/src/contracts/code/wallet.js index 10e2c5699..381bd153f 100644 --- a/js/src/contracts/code/wallet.js +++ b/js/src/contracts/code/wallet.js @@ -15,15 +15,16 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. /** - * @version Solidity v0.4.6 + * @version Solidity v0.4.9 - Optimized * @from https://github.com/ethcore/parity/blob/63137b15482344ff9df634c086abaabed452eadc/js/src/contracts/snippets/enhanced-wallet.sol - * @date 09-Dec-2016 @ 16h00 UTC + * @date 07-Mar-2017 @ 16h00 UTC */ -export const wallet = '0x6060604052346100005760405161041b38038061041b83398101604090815281516020830151918301519201915b604080517f696e697457616c6c657428616464726573735b5d2c75696e743235362c75696e81527f7432353629000000000000000000000000000000000000000000000000000000602080830191909152915190819003602501902084516000829052909173__WalletLibrary_________________________91600281019160049182010290819038829003903960006000600483016000866127105a03f45b505050505050505b610337806100e46000396000f36060604052361561006c5760e060020a60003504632f54bf6e81146101245780634123cb6b146101485780635237509314610167578063659010e714610186578063746c9171146101a5578063c2cf7326146101c4578063c41a360a146101eb578063f1736d8614610217575b6101225b60003411156100c15760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a161011e565b600036111561011e5773__WalletLibrary_________________________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f4156100005750505b5b5b565b005b3461000057610134600435610236565b604080519115158252519081900360200190f35b3461000057610155610297565b60408051918252519081900360200190f35b346100005761015561029d565b60408051918252519081900360200190f35b34610000576101556102a3565b60408051918252519081900360200190f35b34610000576101556102a9565b60408051918252519081900360200190f35b34610000576101346004356024356102af565b604080519115158252519081900360200190f35b34610000576101fb600435610311565b60408051600160a060020a039092168252519081900360200190f35b3461000057610155610331565b60408051918252519081900360200190f35b600073__WalletLibrary_________________________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f4156100005750506040515190505b919050565b60015481565b60045481565b60035481565b60005481565b600073__WalletLibrary_________________________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f4156100005750506040515190505b92915050565b6000600582600101610100811015610000570160005b505490505b919050565b6002548156'; -export const walletLibrary = '0x606060405234610000575b611381806100186000396000f3606060405236156100da5760e060020a6000350463173825d981146100df5780632f54bf6e146100f157806352375093146101155780635c52c2f514610134578063659010e7146101435780637065cb4814610162578063797af627146101745780639da5e0eb14610198578063b20d30a9146101aa578063b61d27f6146101bc578063b75c7dc614610227578063ba51a6df14610239578063c2cf73261461024b578063c57c5f6014610272578063cbf0b0c0146102c6578063e46dcfeb146102d8578063f00d4b5d14610331578063f1736d8614610346575b610000565b34610000576100ef600435610365565b005b3461000057610101600435610452565b604080519115158252519081900360200190f35b3461000057610122610473565b60408051918252519081900360200190f35b34610000576100ef610479565b005b34610000576101226104b0565b60408051918252519081900360200190f35b34610000576100ef6004356104b6565b005b34610000576101016004356105a5565b604080519115158252519081900360200190f35b34610000576100ef60043561081e565b005b34610000576100ef600435610832565b005b3461000057604080516020600460443581810135601f810184900484028501840190955284845261010194823594602480359560649492939190920191819084018382808284375094965061086a95505050505050565b604080519115158252519081900360200190f35b34610000576100ef600435610bcc565b005b34610000576100ef600435610c77565b005b3461000057610101600435602435610cf9565b604080519115158252519081900360200190f35b34610000576100ef6004808035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496505093359350610d4e92505050565b005b34610000576100ef600435610e13565b005b34610000576100ef60048080359060200190820180359060200190808060200260200160405190810160405280939291908181526020018383602002808284375094965050843594602001359350610e5192505050565b005b34610000576100ef600435602435610e6a565b005b3461000057610122610f63565b60408051918252519081900360200190f35b600060003660405180838380828437820191505092505050604051809103902061038e81610f69565b1561044b57600160a060020a0383166000908152610105602052604090205491508115156103bb5761044b565b60016001540360005411156103cf5761044b565b6000600583610100811015610000570160005b5055600160a060020a03831660009081526101056020526040812055610406611108565b61040e6111dc565b60408051600160a060020a038516815290517f58619076adf5bb0943d100ef88d52d7c3fd691b19d3a9071b555b651fbf418da9181900360200190a15b5b5b505050565b600160a060020a03811660009081526101056020526040812054115b919050565b60045481565b6000366040518083838082843782019150509250505060405180910390206104a081610f69565b156104ab5760006003555b5b5b50565b60035481565b6000366040518083838082843782019150509250505060405180910390206104dd81610f69565b1561059f576104eb82610452565b156104f55761059f565b6104fd611108565b60015460fa9010610510576105106111dc565b5b60015460fa90106105215761059f565b60018054810190819055600160a060020a03831690600590610100811015610000570160005b5055600154600160a060020a03831660008181526101056020908152604091829020939093558051918252517f994a936646fe87ffe4f1e469d3d6aa417d6b855598397f323de5b449f765f0c3929181900390910190a15b5b5b5050565b6000816105b181610f69565b156108155760008381526101086020526040902054600160a060020a0316156108155760008381526101086020526040908190208054600180830154935160029384018054600160a060020a0390941695949093919283928592918116156101000260001901160480156106665780601f1061063b57610100808354040283529160200191610666565b820191906000526020600020905b81548152906001019060200180831161064957829003601f168201915b505091505060006040518083038185876185025a03f15050506000848152610108602090815260409182902060018082015482548551600160a060020a033381811683529682018c905296810183905295166060860181905260a06080870181815260029586018054958616156101000260001901909516959095049087018190527fe7c957c06e9a662c1a6c77366179f5b702b97651dc28eee7d5bf1dff6e40bb4a975094958a95929491939290919060c08301908490801561076b5780601f106107405761010080835404028352916020019161076b565b820191906000526020600020905b81548152906001019060200180831161074e57829003601f168201915b5050965050505050505060405180910390a1600083815261010860205260408120805473ffffffffffffffffffffffffffffffffffffffff19168155600180820183905560028083018054858255939493909281161561010002600019011604601f8190106107da575061080c565b601f01602090049060005260206000209081019061080c91905b8082111561080857600081556001016107f4565b5090565b5b505050600191505b5b5b5b50919050565b600281905561082b61130b565b6004555b50565b60003660405180838380828437820191505092505050604051809103902061085981610f69565b1561059f5760028290555b5b5b5050565b6000600061087733610452565b15610bc05761088584611315565b156109bc577f92ca3a80853e6663fa31fa10b99225f18d4902939b4c53a9caae9043f6efd004338587866040518085600160a060020a0316815260200184815260200183600160a060020a03168152602001806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156109335780820380516001836020036101000a031916815260200191505b509550505050505060405180910390a184600160a060020a03168484604051808280519060200190808383829060006004602084601f0104600302600f01f150905090810190601f16801561099c5780820380516001836020036101000a031916815260200191505b5091505060006040518083038185876185025a03f1925050509150610bc0565b600036436040518084848082843782019150508281526020019350505050604051809103902090506109ed816105a5565b158015610a10575060008181526101086020526040902054600160a060020a0316155b15610bc057600081815261010860209081526040822080546c01000000000000000000000000808a020473ffffffffffffffffffffffffffffffffffffffff199091161781556001808201889055865160029283018054818752958590209095601f9381161561010002600019011693909304820184900483019390929190880190839010610aaa57805160ff1916838001178555610ad7565b82800160010185558215610ad7579182015b82811115610ad7578251825591602001919060010190610abc565b5b50610af89291505b8082111561080857600081556001016107f4565b5090565b50507f1733cbb53659d713b79580f79f3f9ff215f78a7c7aa45890f3b89fc5cddfbf328133868887604051808660001916815260200185600160a060020a0316815260200184815260200183600160a060020a03168152602001806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f168015610bae5780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390a15b5b5b5b5b509392505050565b600160a060020a033316600090815261010560205260408120549080821515610bf457610c70565b50506000828152610106602052604081206001810154600284900a929083161115610c705780546001908101825581018054839003905560408051600160a060020a03331681526020810186905281517fc7fb647e59b18047309aa15aad418e5d7ca96d173ad704f1031a2c3d7591734b929181900390910190a15b5b50505050565b600036604051808383808284378201915050925050506040518091039020610c9e81610f69565b1561059f57600154821115610cb25761059f565b6000829055610cbf611108565b6040805183815290517facbdb084c721332ac59f9b8e392196c9eb0e4932862da8eb9beaf0dad4f550da9181900360200190a15b5b5b5050565b600082815261010660209081526040808320600160a060020a038516845261010590925282205482811515610d315760009350610d45565b8160020a9050808360010154166000141593505b50505092915050565b815160019081019055600033600160a060020a03166006825b505550600160a060020a033316600090815261010560205260408120600190558181555b825181101561044b57828181518110156100005790602001906020020151600160a060020a0316600582600201610100811015610000570160005b5081905550806002016101056000858481518110156100005790602001906020020151600160a060020a03168152602001908152602001600020819055505b600101610d8b565b5b505050565b600036604051808383808284378201915050925050506040518091039020610e3a81610f69565b1561059f5781600160a060020a0316ff5b5b5b5050565b610e5b8383610d4e565b61044b8161081e565b5b505050565b6000600036604051808383808284378201915050925050506040518091039020610e9381610f69565b15610c7057610ea183610452565b15610eab57610c70565b600160a060020a038416600090815261010560205260409020549150811515610ed357610c70565b610edb611108565b82600160a060020a0316600583610100811015610000570160005b5055600160a060020a0380851660008181526101056020908152604080832083905593871680835291849020869055835192835282015281517fb532073b38c83145e3e5135377a08bf9aab55bc0fd7c1179cd4fb995d2a5159c929181900390910190a15b5b5b50505050565b60025481565b600160a060020a033316600090815261010560205260408120548180821515610f91576110fe565b60008581526101066020526040902080549092501515611025576000805483556001808401919091556101078054918201808255828015829011610ffa57600083815260209020610ffa9181019083015b8082111561080857600081556001016107f4565b5090565b5b50505060028301819055610107805487929081101561000057906000526020600020900160005b50555b8260020a905080826001015416600014156110fe5760408051600160a060020a03331681526020810187905281517fe1c52dc63b719ade82e8bea94cc41a0d5d28e4aaf536adb5e9cccc9ff8c1aeda929181900390910190a18154600190116110eb5760008581526101066020526040902060020154610107805490919081101561000057906000526020600020900160005b5060009081905585815261010660205260408120818155600180820183905560029091019190915593506110fe566110fe565b8154600019018255600182018054821790555b5b5b505050919050565b6101075460005b818110156111855761010781815481101561000057906000526020600020900160005b50541561117c57610106600061010783815481101561000057906000526020600020900160005b505481526020810191909152604001600090812081815560018101829055600201555b5b60010161110f565b610107805460008083559190915261044b907f47c4908e245f386bfc1825973249847f4053a761ddb4880ad63c323a7b5a2a25908101905b8082111561080857600081556001016107f4565b5090565b5b505b5050565b60015b6001548110156104ab575b6001548110801561120c5750600581610100811015610000570160005b505415155b15611219576001016111ea565b5b600160015411801561123e57506005600154610100811015610000570160005b5054155b15611252576001805460001901905561121a565b6001548110801561127657506005600154610100811015610000570160005b505415155b80156112925750600581610100811015610000570160005b5054155b15611302576005600154610100811015610000570160005b5054600582610100811015610000570160005b5055806101056000600583610100811015610000570160005b505481526020019081526020016000208190555060006005600154610100811015610000570160005b50555b6111df565b5b50565b6201518042045b90565b600061132033610452565b1561046e5760045461133061130b565b111561134757600060035561134361130b565b6004555b600354828101108015906113615750600254826003540111155b1561137657506003805482019055600161046e565b5060005b5b5b91905056'; +export const wallet = '0x6060604052341561000c57fe5b60405161048538038061048583398101604090815281516020830151918301519201915b604080517f696e697457616c6c657428616464726573735b5d2c75696e743235362c75696e81527f7432353629000000000000000000000000000000000000000000000000000000602080830191909152915190819003602501902084516000829052909173_____________WalletLibrary______________91600281019160049182010290819038829003903960006000600483016000866127105a03f45b505050505050505b61039d806100e86000396000f300606060405236156100725763ffffffff60e060020a6000350416632f54bf6e811461012d5780634123cb6b1461015d578063523750931461017f578063659010e7146101a1578063746c9171146101c3578063c2cf7326146101e5578063c41a360a14610218578063f1736d8614610247575b61012b5b60003411156100c75760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a1610127565b60003611156101275773_____________WalletLibrary______________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f4151561012457fe5b50505b5b5b565b005b341561013557fe5b610149600160a060020a0360043516610269565b604080519115158252519081900360200190f35b341561016557fe5b61016d6102cd565b60408051918252519081900360200190f35b341561018757fe5b61016d6102d3565b60408051918252519081900360200190f35b34156101a957fe5b61016d6102d9565b60408051918252519081900360200190f35b34156101cb57fe5b61016d6102df565b60408051918252519081900360200190f35b34156101ed57fe5b610149600435600160a060020a03602435166102e5565b604080519115158252519081900360200190f35b341561022057fe5b61022b60043561034a565b60408051600160a060020a039092168252519081900360200190f35b341561024f57fe5b61016d61036b565b60408051918252519081900360200190f35b600073_____________WalletLibrary______________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f415156102bf57fe5b50506040515190505b919050565b60015481565b60045481565b60035481565b60005481565b600073_____________WalletLibrary______________600160a060020a0316600036600060405160200152604051808383808284378201915050925050506020604051808303818560325a03f4151561033b57fe5b50506040515190505b92915050565b6000600560018301610100811061035d57fe5b0160005b505490505b919050565b600254815600a165627a7a723058204a75c2f5c8009054bd9e9998e8bb6f4bca0b201484709f357b482793957c47130029'; +export const walletLibrary = '0x6060604052341561000c57fe5b5b6116d88061001c6000396000f300606060405236156101015763ffffffff60e060020a600035041663173825d981146101575780632f54bf6e146101755780634123cb6b146101a557806352375093146101c75780635c52c2f5146101e9578063659010e7146101fb5780637065cb481461021d578063746c91711461023b578063797af6271461025d5780639da5e0eb14610284578063b20d30a914610299578063b61d27f6146102ae578063b75c7dc6146102ec578063ba51a6df14610301578063c2cf732614610316578063c41a360a14610349578063c57c5f6014610378578063cbf0b0c0146103cf578063e46dcfeb146103ed578063f00d4b5d14610449578063f1736d861461046d575b6101555b60003411156101525760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a15b5b565b005b341561015f57fe5b610155600160a060020a036004351661048f565b005b341561017d57fe5b610191600160a060020a036004351661057d565b604080519115158252519081900360200190f35b34156101ad57fe5b6101b561059e565b60408051918252519081900360200190f35b34156101cf57fe5b6101b56105a4565b60408051918252519081900360200190f35b34156101f157fe5b6101556105aa565b005b341561020357fe5b6101b56105e1565b60408051918252519081900360200190f35b341561022557fe5b610155600160a060020a03600435166105e7565b005b341561024357fe5b6101b56106d7565b60408051918252519081900360200190f35b341561026557fe5b6101916004356106dd565b604080519115158252519081900360200190f35b341561028c57fe5b610155600435610a2f565b005b34156102a157fe5b610155600435610a43565b005b34156102b657fe5b6101b560048035600160a060020a0316906024803591604435918201910135610a7b565b60408051918252519081900360200190f35b34156102f457fe5b610155600435610d5d565b005b341561030957fe5b610155600435610e08565b005b341561031e57fe5b610191600435600160a060020a0360243516610e8a565b604080519115158252519081900360200190f35b341561035157fe5b61035c600435610edf565b60408051600160a060020a039092168252519081900360200190f35b341561038057fe5b6101556004808035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496505093359350610f0092505050565b005b34156103d757fe5b610155600160a060020a0360043516610fd4565b005b34156103f557fe5b6101556004808035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496505084359460200135935061101292505050565b005b341561045157fe5b610155600160a060020a036004358116906024351661102b565b005b341561047557fe5b6101b5611125565b60408051918252519081900360200190f35b60006000366040518083838082843782019150509250505060405180910390206104b88161112b565b1561057657600160a060020a0383166000908152610105602052604090205491508115156104e557610576565b60016001540360005411156104f957610576565b6000600583610100811061050957fe5b0160005b5055600160a060020a03831660009081526101056020526040812055610531611296565b610539611386565b60408051600160a060020a038516815290517f58619076adf5bb0943d100ef88d52d7c3fd691b19d3a9071b555b651fbf418da9181900360200190a15b5b5b505050565b600160a060020a03811660009081526101056020526040812054115b919050565b60015481565b60045481565b6000366040518083838082843782019150509250505060405180910390206105d18161112b565b156105dc5760006003555b5b5b50565b60035481565b60003660405180838380828437820191505092505050604051809103902061060e8161112b565b156106d15761061c8261057d565b15610626576106d1565b61062e611296565b60015460fa901061064157610641611386565b5b60015460fa9010610652576106d1565b60018054810190819055600160a060020a03831690600590610100811061067557fe5b0160005b5055600154600160a060020a03831660008181526101056020908152604091829020939093558051918252517f994a936646fe87ffe4f1e469d3d6aa417d6b855598397f323de5b449f765f0c3929181900390910190a15b5b5b5050565b60005481565b60006000826106eb8161112b565b15610a255760008481526101086020526040902054600160a060020a031615158061072757506000848152610108602052604090206001015415155b80610754575060008481526101086020526040902060029081015461010060018216150260001901160415155b15610a255760008481526101086020526040902054600160a060020a0316151561082c57600084815261010860209081526040918290206001808201546002928301805486516000199482161561010002949094011693909304601f810185900485028301850190955284825261082594909391929183018282801561081b5780601f106107f05761010080835404028352916020019161081b565b820191906000526020600020905b8154815290600101906020018083116107fe57829003601f168201915b50505050506114c2565b91506108e3565b60008481526101086020526040908190208054600180830154935160029384018054600160a060020a0390941695949093919283928592918116156101000260001901160480156108be5780601f10610893576101008083540402835291602001916108be565b820191906000526020600020905b8154815290600101906020018083116108a157829003601f168201915b505091505060006040518083038185876185025a03f19250505015156108e357610000565b5b6000848152610108602090815260409182902060018082015482548551600160a060020a033381811683529682018c90529681018390529086166060820181905295881660a082015260c06080820181815260029586018054958616156101000260001901909516959095049082018190527fe3a3a4111a84df27d76b68dc721e65c7711605ea5eee4afd3a9c58195217365c968b959394909390928a9290919060e0830190859080156109d95780601f106109ae576101008083540402835291602001916109d9565b820191906000526020600020905b8154815290600101906020018083116109bc57829003601f168201915b505097505050505050505060405180910390a16000848152610108602052604081208054600160a060020a03191681556001810182905590610a1e6002830182611557565b5050600192505b5b5b5b5050919050565b6002819055610a3c6114dc565b6004555b50565b600036604051808383808284378201915050925050506040518091039020610a6a8161112b565b156106d15760028290555b5b5b5050565b60006000610a883361057d565b15610d505782158015610a9f5750610a9f856114eb565b5b80610aad57506000546001145b15610bef57600160a060020a0386161515610b0357610afc8585858080601f016020809104026020016040519081016040528093929190818152602001838380828437506114c2945050505050565b9050610b43565b85600160a060020a03168585856040518083838082843782019150509250505060006040518083038185876185025a03f1925050501515610b4357610000565b5b7f9738cd1a8777c86b011f7b01d87d484217dc6ab5154a9d41eda5d14af8caf2923386888787866040518087600160a060020a0316600160a060020a0316815260200186815260200185600160a060020a0316600160a060020a031681526020018060200183600160a060020a0316600160a060020a0316815260200182810382528585828181526020019250808284376040519201829003995090975050505050505050a1610d50565b600036436040518084848082843791909101928352505060408051602092819003830190206000818152610108909352912054909450600160a060020a0316159150508015610c4e575060008281526101086020526040902060010154155b8015610c7b5750600082815261010860205260409020600290810154610100600182161502600019011604155b15610cbf576000828152610108602052604090208054600160a060020a031916600160a060020a03881617815560018101869055610cbd90600201858561159f565b505b610cc8826106dd565b1515610d505760408051838152600160a060020a033381811660208401529282018890528816606082015260a0608082018181529082018690527f1733cbb53659d713b79580f79f3f9ff215f78a7c7aa45890f3b89fc5cddfbf32928592909189918b918a918a9160c082018484808284376040519201829003995090975050505050505050a15b5b5b5b5b50949350505050565b600160a060020a033316600090815261010560205260408120549080821515610d8557610e01565b50506000828152610106602052604081206001810154600284900a929083161115610e015780546001908101825581018054839003905560408051600160a060020a03331681526020810186905281517fc7fb647e59b18047309aa15aad418e5d7ca96d173ad704f1031a2c3d7591734b929181900390910190a15b5b50505050565b600036604051808383808284378201915050925050506040518091039020610e2f8161112b565b156106d157600154821115610e43576106d1565b6000829055610e50611296565b6040805183815290517facbdb084c721332ac59f9b8e392196c9eb0e4932862da8eb9beaf0dad4f550da9181900360200190a15b5b5b5050565b600082815261010660209081526040808320600160a060020a038516845261010590925282205482811515610ec25760009350610ed6565b8160020a9050808360010154166000141593505b50505092915050565b60006005600183016101008110610ef257fe5b0160005b505490505b919050565b815160019081018155600090600160a060020a033316906005905b0160005b505550600160a060020a033316600090815261010560205260408120600190555b8251811015610fc9578281815181101515610f5757fe5b60209081029091010151600160a060020a03166005600283016101008110610f7b57fe5b0160005b50819055508060020161010560008584815181101515610f9b57fe5b90602001906020020151600160a060020a03168152602001908152602001600020819055505b600101610f40565b60008290555b505050565b600036604051808383808284378201915050925050506040518091039020610ffb8161112b565b156106d15781600160a060020a0316ff5b5b5b5050565b61101b81610a2f565b6105768383610f00565b5b505050565b60006000366040518083838082843782019150509250505060405180910390206110548161112b565b15610e01576110628361057d565b1561106c57610e01565b600160a060020a03841660009081526101056020526040902054915081151561109457610e01565b61109c611296565b600160a060020a03831660058361010081106110b457fe5b0160005b5055600160a060020a0380851660008181526101056020908152604080832083905593871680835291849020869055835192835282015281517fb532073b38c83145e3e5135377a08bf9aab55bc0fd7c1179cd4fb995d2a5159c929181900390910190a15b5b5b50505050565b60025481565b600160a060020a0333166000908152610105602052604081205481808215156111535761128c565b600085815261010660205260409020805490925015156111b65760008054835560018084019190915561010780549161118e9190830161161e565b60028301819055610107805487929081106111a557fe5b906000526020600020900160005b50555b8260020a9050808260010154166000141561128c5760408051600160a060020a03331681526020810187905281517fe1c52dc63b719ade82e8bea94cc41a0d5d28e4aaf536adb5e9cccc9ff8c1aeda929181900390910190a181546001901161127957600085815261010660205260409020600201546101078054909190811061123c57fe5b906000526020600020900160005b50600090819055858152610106602052604081208181556001808201839055600290910191909155935061128c565b8154600019018255600182018054821790555b5b5b505050919050565b6101075460005b81811015611374576101086000610107838154811015156112ba57fe5b906000526020600020900160005b50548152602081019190915260400160009081208054600160a060020a031916815560018101829055906112ff6002830182611557565b505061010780548290811061131057fe5b906000526020600020900160005b50541561136b5761010660006101078381548110151561133a57fe5b906000526020600020900160005b505481526020810191909152604001600090812081815560018101829055600201555b5b60010161129d565b6106d16101076000611648565b5b5050565b60015b6001548110156105dc575b600154811080156113b7575060058161010081106113ae57fe5b0160005b505415155b156113c457600101611394565b5b60016001541180156113eb575060015460059061010081106113e357fe5b0160005b5054155b156113ff57600180546000190190556113c4565b600154811080156114255750600154600590610100811061141c57fe5b0160005b505415155b80156114425750600581610100811061143a57fe5b0160005b5054155b156114b957600154600590610100811061145857fe5b0160005b5054600582610100811061146c57fe5b0160005b5055806101056000600583610100811061148657fe5b0160005b505481526020019081526020016000208190555060006005600154610100811015156114b257fe5b0160005b50555b611389565b5b50565b600081516020830184f09050803b15610000575b92915050565b600062015180425b0490505b90565b60006114f63361057d565b15610599576004546115066114dc565b111561151d5760006003556115196114dc565b6004555b600354828101108015906115375750600254826003540111155b1561154c575060038054820190556001610599565b5060005b5b5b919050565b50805460018160011615610100020316600290046000825580601f1061157d57506105dc565b601f0160209004906000526020600020908101906105dc919061166a565b5b50565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106115e05782800160ff1982351617855561160d565b8280016001018555821561160d579182015b8281111561160d5782358255916020019190600101906115f2565b5b5061161a92915061166a565b5090565b8154818355818115116105765760008381526020902061057691810190830161166a565b5b505050565b50805460008255906000526020600020908101906105dc919061166a565b5b50565b6114e891905b8082111561161a5760008155600101611670565b5090565b90565b6114e891905b8082111561161a5760008155600101611670565b5090565b905600a165627a7a723058206560ca68304798da7e3be68397368a30b63db1453ff138ff8f765e80080025af0029'; +export const walletLibraryABI = '[{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"removeOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_numOwners","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_lastDay","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"resetSpentToday","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_spentToday","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"addOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_required","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_h","type":"bytes32"}],"name":"confirm","outputs":[{"name":"o_success","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_limit","type":"uint256"}],"name":"initDaylimit","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_newLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"o_hash","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_operation","type":"bytes32"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_newRequired","type":"uint256"}],"name":"changeRequirement","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_operation","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"hasConfirmed","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"ownerIndex","type":"uint256"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"}],"name":"initMultiowned","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"}],"name":"kill","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"},{"name":"_daylimit","type":"uint256"}],"name":"initWallet","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"changeOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_dailyLimit","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"payable":true,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Confirmation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Revoke","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"},{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"}],"name":"OwnerRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newRequirement","type":"uint256"}],"name":"RequirementChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"},{"indexed":false,"name":"created","type":"address"}],"name":"SingleTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"},{"indexed":false,"name":"created","type":"address"}],"name":"MultiTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"initiator","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"ConfirmationNeeded","type":"event"}]'; export const walletSourceURL = 'https://github.com/ethcore/parity/blob/63137b15482344ff9df634c086abaabed452eadc/js/src/contracts/snippets/enhanced-wallet.sol'; export const walletLibraryRegKey = 'walletLibrary'; // Used if no Wallet Library found in registry... -// Compiled from `wallet.sol` using Solidity v0.4.6 -export const fullWalletCode = '0x606060405234610000576040516113bb3803806113bb83398101604090815281516020830151918301519201915b805b83835b815160019081019055600033600160a060020a03166003825b505550600160a060020a033316600090815261010260205260408120600190555b82518110156100ee57828181518110156100005790602001906020020151600160a060020a0316600282600201610100811015610000570160005b5081905550806002016101026000858481518110156100005790602001906020020151600160a060020a03168152602001908152602001600020819055505b60010161006c565b60008290555b50505061010581905561011264010000000061127861012182021704565b610107555b505b50505061012b565b6201518042045b90565b611282806101396000396000f3606060405236156100da5760e060020a6000350463173825d981146101305780632f54bf6e146101425780634123cb6b1461016657806352375093146101855780635c52c2f5146101a4578063659010e7146101b35780637065cb48146101d2578063746c9171146101e4578063797af62714610203578063b20d30a914610227578063b61d27f614610239578063b75c7dc61461026b578063ba51a6df1461027d578063c2cf73261461028f578063c41a360a146102b6578063cbf0b0c0146102e2578063f00d4b5d146102f4578063f1736d8614610309575b61012e5b600034111561012b5760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a15b5b565b005b346100005761012e600435610328565b005b3461000057610152600435610415565b604080519115158252519081900360200190f35b3461000057610173610436565b60408051918252519081900360200190f35b346100005761017361043c565b60408051918252519081900360200190f35b346100005761012e610443565b005b346100005761017361047b565b60408051918252519081900360200190f35b346100005761012e600435610482565b005b3461000057610173610571565b60408051918252519081900360200190f35b3461000057610152600435610577565b604080519115158252519081900360200190f35b346100005761012e6004356107e3565b005b34610000576101736004803590602480359160443591820191013561081c565b60408051918252519081900360200190f35b346100005761012e600435610ab3565b005b346100005761012e600435610b5e565b005b3461000057610152600435602435610be0565b604080519115158252519081900360200190f35b34610000576102c6600435610c35565b60408051600160a060020a039092168252519081900360200190f35b346100005761012e600435610c55565b005b346100005761012e600435602435610c93565b005b3461000057610173610d8c565b60408051918252519081900360200190f35b600060003660405180838380828437820191505092505050604051809103902061035181610d93565b1561040e57600160a060020a03831660009081526101026020526040902054915081151561037e5761040e565b60016001540360005411156103925761040e565b6000600283610100811015610000570160005b5055600160a060020a038316600090815261010260205260408120556103c9610f32565b6103d1611002565b60408051600160a060020a038516815290517f58619076adf5bb0943d100ef88d52d7c3fd691b19d3a9071b555b651fbf418da9181900360200190a15b5b5b505050565b600160a060020a03811660009081526101026020526040812054115b919050565b60015481565b6101075481565b60003660405180838380828437820191505092505050604051809103902061046a81610d93565b15610476576000610106555b5b5b50565b6101065481565b6000366040518083838082843782019150509250505060405180910390206104a981610d93565b1561056b576104b782610415565b156104c15761056b565b6104c9610f32565b60015460fa90106104dc576104dc611002565b5b60015460fa90106104ed5761056b565b60018054810190819055600160a060020a03831690600290610100811015610000570160005b5055600154600160a060020a03831660008181526101026020908152604091829020939093558051918252517f994a936646fe87ffe4f1e469d3d6aa417d6b855598397f323de5b449f765f0c3929181900390910190a15b5b5b5050565b60005481565b60008161058381610d93565b156107da5760008381526101086020526040902054600160a060020a0316156107da5760008381526101086020526040908190208054600180830154935160029384018054600160a060020a0390941695949093919283928592918116156101000260001901160480156106385780601f1061060d57610100808354040283529160200191610638565b820191906000526020600020905b81548152906001019060200180831161061b57829003601f168201915b505091505060006040518083038185876185025a03f15050506000848152610108602090815260409182902060018082015482548551600160a060020a033381811683529682018c905296810183905295166060860181905260a06080870181815260029586018054958616156101000260001901909516959095049087018190527fe7c957c06e9a662c1a6c77366179f5b702b97651dc28eee7d5bf1dff6e40bb4a975094958a95929491939290919060c08301908490801561073d5780601f106107125761010080835404028352916020019161073d565b820191906000526020600020905b81548152906001019060200180831161072057829003601f168201915b5050965050505050505060405180910390a16000838152610108602052604081208054600160a060020a0319168155600180820183905560028083018054858255939493909281161561010002600019011604601f81901061079f57506107d1565b601f0160209004906000526020600020908101906107d191905b808211156107cd57600081556001016107b9565b5090565b5b505050600191505b5b5b5b50919050565b60003660405180838380828437820191505092505050604051809103902061080a81610d93565b1561056b576101058290555b5b5b5050565b600061082733610415565b15610aa85761083584611131565b156108f3577f92ca3a80853e6663fa31fa10b99225f18d4902939b4c53a9caae9043f6efd00433858786866040518086600160a060020a0316815260200185815260200184600160a060020a0316815260200180602001828103825284848281815260200192508082843760405192018290039850909650505050505050a184600160a060020a03168484846040518083838082843782019150509250505060006040518083038185876185025a03f15060009350610aa892505050565b6000364360405180848480828437820191505082815260200193505050506040518091039020905061092481610577565b158015610947575060008181526101086020526040902054600160a060020a0316155b15610aa857600081815261010860209081526040822080546c01000000000000000000000000808a0204600160a060020a0319909116178155600180820188905560029182018054818652948490209094601f928116156101000260001901169290920481019290920481019185919087908390106109d15782800160ff198235161785556109fe565b828001600101855582156109fe579182015b828111156109fe5782358255916020019190600101906109e3565b5b50610a1f9291505b808211156107cd57600081556001016107b9565b5090565b50507f1733cbb53659d713b79580f79f3f9ff215f78a7c7aa45890f3b89fc5cddfbf32813386888787604051808760001916815260200186600160a060020a0316815260200185815260200184600160a060020a031681526020018060200182810382528484828181526020019250808284376040519201829003995090975050505050505050a15b5b5b5b949350505050565b600160a060020a033316600090815261010260205260408120549080821515610adb57610b57565b50506000828152610103602052604081206001810154600284900a929083161115610b575780546001908101825581018054839003905560408051600160a060020a03331681526020810186905281517fc7fb647e59b18047309aa15aad418e5d7ca96d173ad704f1031a2c3d7591734b929181900390910190a15b5b50505050565b600036604051808383808284378201915050925050506040518091039020610b8581610d93565b1561056b57600154821115610b995761056b565b6000829055610ba6610f32565b6040805183815290517facbdb084c721332ac59f9b8e392196c9eb0e4932862da8eb9beaf0dad4f550da9181900360200190a15b5b5b5050565b600082815261010360209081526040808320600160a060020a038516845261010290925282205482811515610c185760009350610c2c565b8160020a9050808360010154166000141593505b50505092915050565b6000600282600101610100811015610000570160005b505490505b919050565b600036604051808383808284378201915050925050506040518091039020610c7c81610d93565b1561056b5781600160a060020a0316ff5b5b5b5050565b6000600036604051808383808284378201915050925050506040518091039020610cbc81610d93565b15610b5757610cca83610415565b15610cd457610b57565b600160a060020a038416600090815261010260205260409020549150811515610cfc57610b57565b610d04610f32565b82600160a060020a0316600283610100811015610000570160005b5055600160a060020a0380851660008181526101026020908152604080832083905593871680835291849020869055835192835282015281517fb532073b38c83145e3e5135377a08bf9aab55bc0fd7c1179cd4fb995d2a5159c929181900390910190a15b5b5b50505050565b6101055481565b600160a060020a033316600090815261010260205260408120548180821515610dbb57610f28565b60008581526101036020526040902080549092501515610e4f576000805483556001808401919091556101048054918201808255828015829011610e2457600083815260209020610e249181019083015b808211156107cd57600081556001016107b9565b5090565b5b50505060028301819055610104805487929081101561000057906000526020600020900160005b50555b8260020a90508082600101541660001415610f285760408051600160a060020a03331681526020810187905281517fe1c52dc63b719ade82e8bea94cc41a0d5d28e4aaf536adb5e9cccc9ff8c1aeda929181900390910190a1815460019011610f155760008581526101036020526040902060020154610104805490919081101561000057906000526020600020900160005b506000908190558581526101036020526040812081815560018082018390556002909101919091559350610f2856610f28565b8154600019018255600182018054821790555b5b5b505050919050565b6101045460005b81811015610ff557610108600061010483815481101561000057906000526020600020900160005b50548152602081019190915260400160009081208054600160a060020a0319168155600180820183905560028083018054858255939493909281161561010002600019011604601f819010610fb65750610fe8565b601f016020900490600052602060002090810190610fe891905b808211156107cd57600081556001016107b9565b5090565b5b5050505b600101610f39565b61056b6111a4565b5b5050565b60015b600154811015610476575b600154811080156110325750600281610100811015610000570160005b505415155b1561103f57600101611010565b5b600160015411801561106457506002600154610100811015610000570160005b5054155b156110785760018054600019019055611040565b6001548110801561109c57506002600154610100811015610000570160005b505415155b80156110b85750600281610100811015610000570160005b5054155b15611128576002600154610100811015610000570160005b5054600282610100811015610000570160005b5055806101026000600283610100811015610000570160005b505481526020019081526020016000208190555060006002600154610100811015610000570160005b50555b611005565b5b50565b600061113c33610415565b15610431576101075461114d611278565b111561116657600061010655611161611278565b610107555b610106548281011080159061118357506101055482610106540111155b1561119957506101068054820190556001610431565b5060005b5b5b919050565b6101045460005b818110156112215761010481815481101561000057906000526020600020900160005b50541561121857610103600061010483815481101561000057906000526020600020900160005b505481526020810191909152604001600090812081815560018101829055600201555b5b6001016111ab565b610104805460008083559190915261040e907f4c0be60200faa20559308cb7b5a1bb3255c16cb1cab91f525b5ae7a03d02fabe908101905b808211156107cd57600081556001016107b9565b5090565b5b505b5050565b6201518042045b9056'; +// Compiled from `wallet.sol` using Solidity v0.4.9 - Optimized +export const fullWalletCode = '0x6060604052341561000c57fe5b60405161166d38038061166d83398101604090815281516020830151918301519201915b805b83835b815160019081018155600090600160a060020a033316906002905b0160005b505550600160a060020a033316600090815261010260205260408120600190555b82518110156100fd57828181518110151561008c57fe5b60209081029091010151600160a060020a0316600282810161010081106100af57fe5b0160005b508190555080600201610102600085848151811015156100cf57fe5b90602001906020020151600160a060020a03168152602001908152602001600020819055505b600101610075565b60008290555b50505061010581905561012164010000000061138f61013082021704565b610107555b505b50505061013f565b600062015180425b0490505b90565b61151f8061014e6000396000f300606060405236156100e05763ffffffff60e060020a600035041663173825d981146101365780632f54bf6e146101545780634123cb6b1461018457806352375093146101a65780635c52c2f5146101c8578063659010e7146101da5780637065cb48146101fc578063746c91711461021a578063797af6271461023c578063b20d30a914610263578063b61d27f614610278578063b75c7dc6146102b6578063ba51a6df146102cb578063c2cf7326146102e0578063c41a360a14610313578063cbf0b0c014610342578063f00d4b5d14610360578063f1736d8614610384575b6101345b60003411156101315760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a15b5b565b005b341561013e57fe5b610134600160a060020a03600435166103a6565b005b341561015c57fe5b610170600160a060020a0360043516610494565b604080519115158252519081900360200190f35b341561018c57fe5b6101946104b5565b60408051918252519081900360200190f35b34156101ae57fe5b6101946104bb565b60408051918252519081900360200190f35b34156101d057fe5b6101346104c2565b005b34156101e257fe5b6101946104fa565b60408051918252519081900360200190f35b341561020457fe5b610134600160a060020a0360043516610501565b005b341561022257fe5b6101946105f1565b60408051918252519081900360200190f35b341561024457fe5b6101706004356105f7565b604080519115158252519081900360200190f35b341561026b57fe5b610134600435610949565b005b341561028057fe5b61019460048035600160a060020a0316906024803591604435918201910135610982565b60408051918252519081900360200190f35b34156102be57fe5b610134600435610c64565b005b34156102d357fe5b610134600435610d0f565b005b34156102e857fe5b610170600435600160a060020a0360243516610d91565b604080519115158252519081900360200190f35b341561031b57fe5b610326600435610de6565b60408051600160a060020a039092168252519081900360200190f35b341561034a57fe5b610134600160a060020a0360043516610e07565b005b341561036857fe5b610134600160a060020a0360043581169060243516610e45565b005b341561038c57fe5b610194610f3f565b60408051918252519081900360200190f35b60006000366040518083838082843782019150509250505060405180910390206103cf81610f46565b1561048d57600160a060020a0383166000908152610102602052604090205491508115156103fc5761048d565b60016001540360005411156104105761048d565b6000600283610100811061042057fe5b0160005b5055600160a060020a038316600090815261010260205260408120556104486110b1565b610450611132565b60408051600160a060020a038516815290517f58619076adf5bb0943d100ef88d52d7c3fd691b19d3a9071b555b651fbf418da9181900360200190a15b5b5b505050565b600160a060020a03811660009081526101026020526040812054115b919050565b60015481565b6101075481565b6000366040518083838082843782019150509250505060405180910390206104e981610f46565b156104f5576000610106555b5b5b50565b6101065481565b60003660405180838380828437820191505092505050604051809103902061052881610f46565b156105eb5761053682610494565b15610540576105eb565b6105486110b1565b60015460fa901061055b5761055b611132565b5b60015460fa901061056c576105eb565b60018054810190819055600160a060020a03831690600290610100811061058f57fe5b0160005b5055600154600160a060020a03831660008181526101026020908152604091829020939093558051918252517f994a936646fe87ffe4f1e469d3d6aa417d6b855598397f323de5b449f765f0c3929181900390910190a15b5b5b5050565b60005481565b600060008261060581610f46565b1561093f5760008481526101086020526040902054600160a060020a031615158061064157506000848152610108602052604090206001015415155b8061066e575060008481526101086020526040902060029081015461010060018216150260001901160415155b1561093f5760008481526101086020526040902054600160a060020a0316151561074657600084815261010860209081526040918290206001808201546002928301805486516000199482161561010002949094011693909304601f810185900485028301850190955284825261073f9490939192918301828280156107355780601f1061070a57610100808354040283529160200191610735565b820191906000526020600020905b81548152906001019060200180831161071857829003601f168201915b505050505061126e565b91506107fd565b60008481526101086020526040908190208054600180830154935160029384018054600160a060020a0390941695949093919283928592918116156101000260001901160480156107d85780601f106107ad576101008083540402835291602001916107d8565b820191906000526020600020905b8154815290600101906020018083116107bb57829003601f168201915b505091505060006040518083038185876185025a03f19250505015156107fd57610000565b5b6000848152610108602090815260409182902060018082015482548551600160a060020a033381811683529682018c90529681018390529086166060820181905295881660a082015260c06080820181815260029586018054958616156101000260001901909516959095049082018190527fe3a3a4111a84df27d76b68dc721e65c7711605ea5eee4afd3a9c58195217365c968b959394909390928a9290919060e0830190859080156108f35780601f106108c8576101008083540402835291602001916108f3565b820191906000526020600020905b8154815290600101906020018083116108d657829003601f168201915b505097505050505050505060405180910390a16000848152610108602052604081208054600160a060020a03191681556001810182905590610938600283018261139e565b5050600192505b5b5b5b5050919050565b60003660405180838380828437820191505092505050604051809103902061097081610f46565b156105eb576101058290555b5b5b5050565b6000600061098f33610494565b15610c5757821580156109a657506109a685611288565b5b806109b457506000546001145b15610af657600160a060020a0386161515610a0a57610a038585858080601f0160208091040260200160405190810160405280939291908181526020018383808284375061126e945050505050565b9050610a4a565b85600160a060020a03168585856040518083838082843782019150509250505060006040518083038185876185025a03f1925050501515610a4a57610000565b5b7f9738cd1a8777c86b011f7b01d87d484217dc6ab5154a9d41eda5d14af8caf2923386888787866040518087600160a060020a0316600160a060020a0316815260200186815260200185600160a060020a0316600160a060020a031681526020018060200183600160a060020a0316600160a060020a0316815260200182810382528585828181526020019250808284376040519201829003995090975050505050505050a1610c57565b600036436040518084848082843791909101928352505060408051602092819003830190206000818152610108909352912054909450600160a060020a0316159150508015610b55575060008281526101086020526040902060010154155b8015610b825750600082815261010860205260409020600290810154610100600182161502600019011604155b15610bc6576000828152610108602052604090208054600160a060020a031916600160a060020a03881617815560018101869055610bc49060020185856113e6565b505b610bcf826105f7565b1515610c575760408051838152600160a060020a033381811660208401529282018890528816606082015260a0608082018181529082018690527f1733cbb53659d713b79580f79f3f9ff215f78a7c7aa45890f3b89fc5cddfbf32928592909189918b918a918a9160c082018484808284376040519201829003995090975050505050505050a15b5b5b5b5b50949350505050565b600160a060020a033316600090815261010260205260408120549080821515610c8c57610d08565b50506000828152610103602052604081206001810154600284900a929083161115610d085780546001908101825581018054839003905560408051600160a060020a03331681526020810186905281517fc7fb647e59b18047309aa15aad418e5d7ca96d173ad704f1031a2c3d7591734b929181900390910190a15b5b50505050565b600036604051808383808284378201915050925050506040518091039020610d3681610f46565b156105eb57600154821115610d4a576105eb565b6000829055610d576110b1565b6040805183815290517facbdb084c721332ac59f9b8e392196c9eb0e4932862da8eb9beaf0dad4f550da9181900360200190a15b5b5b5050565b600082815261010360209081526040808320600160a060020a038516845261010290925282205482811515610dc95760009350610ddd565b8160020a9050808360010154166000141593505b50505092915050565b60006002600183016101008110610df957fe5b0160005b505490505b919050565b600036604051808383808284378201915050925050506040518091039020610e2e81610f46565b156105eb5781600160a060020a0316ff5b5b5b5050565b6000600036604051808383808284378201915050925050506040518091039020610e6e81610f46565b15610d0857610e7c83610494565b15610e8657610d08565b600160a060020a038416600090815261010260205260409020549150811515610eae57610d08565b610eb66110b1565b600160a060020a0383166002836101008110610ece57fe5b0160005b5055600160a060020a0380851660008181526101026020908152604080832083905593871680835291849020869055835192835282015281517fb532073b38c83145e3e5135377a08bf9aab55bc0fd7c1179cd4fb995d2a5159c929181900390910190a15b5b5b50505050565b6101055481565b600160a060020a033316600090815261010260205260408120548180821515610f6e576110a7565b60008581526101036020526040902080549092501515610fd157600080548355600180840191909155610104805491610fa991908301611465565b6002830181905561010480548792908110610fc057fe5b906000526020600020900160005b50555b8260020a905080826001015416600014156110a75760408051600160a060020a03331681526020810187905281517fe1c52dc63b719ade82e8bea94cc41a0d5d28e4aaf536adb5e9cccc9ff8c1aeda929181900390910190a181546001901161109457600085815261010360205260409020600201546101048054909190811061105757fe5b906000526020600020900160005b5060009081905585815261010360205260408120818155600180820183905560029091019190915593506110a7565b8154600019018255600182018054821790555b5b5b505050919050565b6101045460005b81811015611125576101086000610104838154811015156110d557fe5b906000526020600020900160005b50548152602081019190915260400160009081208054600160a060020a0319168155600181018290559061111a600283018261139e565b50505b6001016110b8565b6105eb6112fb565b5b5050565b60015b6001548110156104f5575b600154811080156111635750600281610100811061115a57fe5b0160005b505415155b1561117057600101611140565b5b60016001541180156111975750600154600290610100811061118f57fe5b0160005b5054155b156111ab5760018054600019019055611170565b600154811080156111d1575060015460029061010081106111c857fe5b0160005b505415155b80156111ee575060028161010081106111e657fe5b0160005b5054155b1561126557600154600290610100811061120457fe5b0160005b5054600282610100811061121857fe5b0160005b5055806101026000600283610100811061123257fe5b0160005b5054815260200190815260200160002081905550600060026001546101008110151561125e57fe5b0160005b50555b611135565b5b50565b600081516020830184f09050803b15610000575b92915050565b600061129333610494565b156104b057610107546112a461138f565b11156112bd576000610106556112b861138f565b610107555b61010654828101108015906112da57506101055482610106540111155b156112f0575061010680548201905560016104b0565b5060005b5b5b919050565b6101045460005b8181101561137d5761010480548290811061131957fe5b906000526020600020900160005b5054156113745761010360006101048381548110151561134357fe5b906000526020600020900160005b505481526020810191909152604001600090812081815560018101829055600201555b5b600101611302565b6105eb610104600061148f565b5b5050565b600062015180425b0490505b90565b50805460018160011615610100020316600290046000825580601f106113c457506104f5565b601f0160209004906000526020600020908101906104f591906114b1565b5b50565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106114275782800160ff19823516178555611454565b82800160010185558215611454579182015b82811115611454578235825591602001919060010190611439565b5b506114619291506114b1565b5090565b81548183558181151161048d5760008381526020902061048d9181019083016114b1565b5b505050565b50805460008255906000526020600020908101906104f591906114b1565b5b50565b61139b91905b8082111561146157600081556001016114b7565b5090565b90565b61139b91905b8082111561146157600081556001016114b7565b5090565b905600a165627a7a723058203a7ac7072dc640002704b704af82b742650362cd55debf72fca105c2b916e01d0029'; diff --git a/js/src/contracts/snippets/enhanced-wallet.sol b/js/src/contracts/snippets/enhanced-wallet.sol index 374eb595f..1f89b1f6f 100644 --- a/js/src/contracts/snippets/enhanced-wallet.sol +++ b/js/src/contracts/snippets/enhanced-wallet.sol @@ -8,453 +8,454 @@ // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the // interior is executed. -pragma solidity ^0.4.6; -contract multisig { - // EVENTS +pragma solidity ^0.4.9; - // this contract can accept a confirmation, in which case - // we record owner and operation (hash) alongside it. - event Confirmation(address owner, bytes32 operation); - event Revoke(address owner, bytes32 operation); +contract WalletEvents { + // EVENTS - // some others are in the case of an owner changing. - event OwnerChanged(address oldOwner, address newOwner); - event OwnerAdded(address newOwner); - event OwnerRemoved(address oldOwner); + // this contract only has six types of events: it can accept a confirmation, in which case + // we record owner and operation (hash) alongside it. + event Confirmation(address owner, bytes32 operation); + event Revoke(address owner, bytes32 operation); - // the last one is emitted if the required signatures change - event RequirementChanged(uint newRequirement); + // some others are in the case of an owner changing. + event OwnerChanged(address oldOwner, address newOwner); + event OwnerAdded(address newOwner); + event OwnerRemoved(address oldOwner); - // Funds has arrived into the wallet (record how much). - event Deposit(address _from, uint value); - // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). - event SingleTransact(address owner, uint value, address to, bytes data); - // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). - event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); - // Confirmation still needed for a transaction. - event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); + // the last one is emitted if the required signatures change + event RequirementChanged(uint newRequirement); + + // Funds has arrived into the wallet (record how much). + event Deposit(address _from, uint value); + // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). + event SingleTransact(address owner, uint value, address to, bytes data, address created); + // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). + event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created); + // Confirmation still needed for a transaction. + event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); } -contract multisigAbi is multisig { - function isOwner(address _addr) returns (bool); +contract WalletAbi { + // Revokes a prior confirmation of the given operation + function revoke(bytes32 _operation) external; - function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool); + // Replaces an owner `_from` with another `_to`. + function changeOwner(address _from, address _to) external; - function confirm(bytes32 _h) returns(bool); + function addOwner(address _owner) external; - // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. - function setDailyLimit(uint _newLimit); + function removeOwner(address _owner) external; - function addOwner(address _owner); + function changeRequirement(uint _newRequired) external; - function removeOwner(address _owner); + function isOwner(address _addr) constant returns (bool); - function changeRequirement(uint _newRequired); + function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool); - // Revokes a prior confirmation of the given operation - function revoke(bytes32 _operation); + // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. + function setDailyLimit(uint _newLimit) external; - function changeOwner(address _from, address _to); - - function execute(address _to, uint _value, bytes _data) returns(bool); + function execute(address _to, uint _value, bytes _data) external returns (bytes32 o_hash); + function confirm(bytes32 _h) returns (bool o_success); } -contract WalletLibrary is multisig { - // TYPES +contract WalletLibrary is WalletEvents { + // TYPES - // struct for the status of a pending operation. - struct PendingState { - uint yetNeeded; - uint ownersDone; - uint index; + // struct for the status of a pending operation. + struct PendingState { + uint yetNeeded; + uint ownersDone; + uint index; + } + + // Transaction structure to remember details of transaction lest it need be saved for a later call. + struct Transaction { + address to; + uint value; + bytes data; + } + + // MODIFIERS + + // simple single-sig function modifier. + modifier onlyowner { + if (isOwner(msg.sender)) + _; + } + // multi-sig function modifier: the operation must have an intrinsic hash in order + // that later attempts can be realised as the same underlying operation and + // thus count as confirmations. + modifier onlymanyowners(bytes32 _operation) { + if (confirmAndCheck(_operation)) + _; + } + + // METHODS + + // gets called when no other function matches + function() payable { + // just being sent some cash? + if (msg.value > 0) + Deposit(msg.sender, msg.value); + } + + // constructor is given number of sigs required to do protected "onlymanyowners" transactions + // as well as the selection of addresses capable of confirming them. + function initMultiowned(address[] _owners, uint _required) { + m_numOwners = _owners.length + 1; + m_owners[1] = uint(msg.sender); + m_ownerIndex[uint(msg.sender)] = 1; + for (uint i = 0; i < _owners.length; ++i) + { + m_owners[2 + i] = uint(_owners[i]); + m_ownerIndex[uint(_owners[i])] = 2 + i; + } + m_required = _required; + } + + // Revokes a prior confirmation of the given operation + function revoke(bytes32 _operation) external { + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + uint ownerIndexBit = 2**ownerIndex; + var pending = m_pending[_operation]; + if (pending.ownersDone & ownerIndexBit > 0) { + pending.yetNeeded++; + pending.ownersDone -= ownerIndexBit; + Revoke(msg.sender, _operation); + } + } + + // Replaces an owner `_from` with another `_to`. + function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_to)) return; + uint ownerIndex = m_ownerIndex[uint(_from)]; + if (ownerIndex == 0) return; + + clearPending(); + m_owners[ownerIndex] = uint(_to); + m_ownerIndex[uint(_from)] = 0; + m_ownerIndex[uint(_to)] = ownerIndex; + OwnerChanged(_from, _to); + } + + function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_owner)) return; + + clearPending(); + if (m_numOwners >= c_maxOwners) + reorganizeOwners(); + if (m_numOwners >= c_maxOwners) + return; + m_numOwners++; + m_owners[m_numOwners] = uint(_owner); + m_ownerIndex[uint(_owner)] = m_numOwners; + OwnerAdded(_owner); + } + + function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + uint ownerIndex = m_ownerIndex[uint(_owner)]; + if (ownerIndex == 0) return; + if (m_required > m_numOwners - 1) return; + + m_owners[ownerIndex] = 0; + m_ownerIndex[uint(_owner)] = 0; + clearPending(); + reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot + OwnerRemoved(_owner); + } + + function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { + if (_newRequired > m_numOwners) return; + m_required = _newRequired; + clearPending(); + RequirementChanged(_newRequired); + } + + // Gets an owner by 0-indexed position (using numOwners as the count) + function getOwner(uint ownerIndex) external constant returns (address) { + return address(m_owners[ownerIndex + 1]); + } + + function isOwner(address _addr) constant returns (bool) { + return m_ownerIndex[uint(_addr)] > 0; + } + + function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) { + var pending = m_pending[_operation]; + uint ownerIndex = m_ownerIndex[uint(_owner)]; + + // make sure they're an owner + if (ownerIndex == 0) return false; + + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + return !(pending.ownersDone & ownerIndexBit == 0); + } + + // constructor - stores initial daily limit and records the present day's index. + function initDaylimit(uint _limit) { + m_dailyLimit = _limit; + m_lastDay = today(); + } + // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. + function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { + m_dailyLimit = _newLimit; + } + // resets the amount already spent today. needs many of the owners to confirm. + function resetSpentToday() onlymanyowners(sha3(msg.data)) external { + m_spentToday = 0; + } + + // constructor - just pass on the owner array to the multiowned and + // the limit to daylimit + function initWallet(address[] _owners, uint _required, uint _daylimit) { + initDaylimit(_daylimit); + initMultiowned(_owners, _required); + } + + // kills the contract sending everything to `_to`. + function kill(address _to) onlymanyowners(sha3(msg.data)) external { + suicide(_to); + } + + // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. + // If not, goes into multisig process. We provide a hash on return to allow the sender to provide + // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value + // and _data arguments). They still get the option of using them if they want, anyways. + function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) { + // first, take the opportunity to check that we're under the daily limit. + if ((_data.length == 0 && underLimit(_value)) || m_required == 1) { + // yes - just execute the call. + address created; + if (_to == 0) { + created = create(_value, _data); + } else { + if (!_to.call.value(_value)(_data)) + throw; + } + SingleTransact(msg.sender, _value, _to, _data, created); + } else { + // determine our operation hash. + o_hash = sha3(msg.data, block.number); + // store if it's new + if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) { + m_txs[o_hash].to = _to; + m_txs[o_hash].value = _value; + m_txs[o_hash].data = _data; + } + if (!confirm(o_hash)) { + ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data); + } + } + } + + function create(uint _value, bytes _code) internal returns (address o_addr) { + assembly { + o_addr := create(_value, add(_code, 0x20), mload(_code)) + jumpi(invalidJumpLabel, iszero(extcodesize(o_addr))) + } + } + + // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order + // to determine the body of the transaction from the hash provided. + function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) { + if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) { + address created; + if (m_txs[_h].to == 0) { + created = create(m_txs[_h].value, m_txs[_h].data); + } else { + if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data)) + throw; + } + + MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created); + delete m_txs[_h]; + return true; + } + } + + // INTERNAL METHODS + + function confirmAndCheck(bytes32 _operation) internal returns (bool) { + // determine what index the present sender is: + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + + var pending = m_pending[_operation]; + // if we're not yet working on this operation, switch over and reset the confirmation status. + if (pending.yetNeeded == 0) { + // reset count of confirmations needed. + pending.yetNeeded = m_required; + // reset which owners have confirmed (none) - set our bitmap to 0. + pending.ownersDone = 0; + pending.index = m_pendingIndex.length++; + m_pendingIndex[pending.index] = _operation; + } + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + // make sure we (the message sender) haven't confirmed this operation previously. + if (pending.ownersDone & ownerIndexBit == 0) { + Confirmation(msg.sender, _operation); + // ok - check if count is enough to go ahead. + if (pending.yetNeeded <= 1) { + // enough confirmations: reset and run interior. + delete m_pendingIndex[m_pending[_operation].index]; + delete m_pending[_operation]; + return true; + } + else + { + // not enough: record that this owner in particular confirmed. + pending.yetNeeded--; + pending.ownersDone |= ownerIndexBit; + } + } + } + + function reorganizeOwners() private { + uint free = 1; + while (free < m_numOwners) + { + while (free < m_numOwners && m_owners[free] != 0) free++; + while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; + if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) + { + m_owners[free] = m_owners[m_numOwners]; + m_ownerIndex[m_owners[free]] = free; + m_owners[m_numOwners] = 0; + } + } + } + + // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and + // returns true. otherwise just returns false. + function underLimit(uint _value) internal onlyowner returns (bool) { + // reset the spend limit if we're on a different day to last time. + if (today() > m_lastDay) { + m_spentToday = 0; + m_lastDay = today(); + } + // check to see if there's enough left - if so, subtract and return true. + // overflow protection // dailyLimit check + if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { + m_spentToday += _value; + return true; + } + return false; + } + + // determines today's index. + function today() private constant returns (uint) { return now / 1 days; } + + function clearPending() internal { + uint length = m_pendingIndex.length; + + for (uint i = 0; i < length; ++i) { + delete m_txs[m_pendingIndex[i]]; + + if (m_pendingIndex[i] != 0) + delete m_pending[m_pendingIndex[i]]; } - // Transaction structure to remember details of transaction lest it need be saved for a later call. - struct Transaction { - address to; - uint value; - bytes data; - } + delete m_pendingIndex; + } - /****************************** - ***** MULTI OWNED SECTION **** - ******************************/ + // FIELDS + address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; - // MODIFIERS + // the number of owners that must confirm the same operation before it is run. + uint public m_required; + // pointer used to find a free slot in m_owners + uint public m_numOwners; - // simple single-sig function modifier. - modifier onlyowner { - if (isOwner(msg.sender)) - _; - } - // multi-sig function modifier: the operation must have an intrinsic hash in order - // that later attempts can be realised as the same underlying operation and - // thus count as confirmations. - modifier onlymanyowners(bytes32 _operation) { - if (confirmAndCheck(_operation)) - _; - } + uint public m_dailyLimit; + uint public m_spentToday; + uint public m_lastDay; - // METHODS + // list of owners + uint[256] m_owners; - // constructor is given number of sigs required to do protected "onlymanyowners" transactions - // as well as the selection of addresses capable of confirming them. - function initMultiowned(address[] _owners, uint _required) { - m_numOwners = _owners.length + 1; - m_owners[1] = uint(msg.sender); - m_ownerIndex[uint(msg.sender)] = 1; - m_required = _required; + uint constant c_maxOwners = 250; + // index on the list of owners to allow reverse lookup + mapping(uint => uint) m_ownerIndex; + // the ongoing operations. + mapping(bytes32 => PendingState) m_pending; + bytes32[] m_pendingIndex; - for (uint i = 0; i < _owners.length; ++i) - { - m_owners[2 + i] = uint(_owners[i]); - m_ownerIndex[uint(_owners[i])] = 2 + i; - } - } - - // Revokes a prior confirmation of the given operation - function revoke(bytes32 _operation) { - uint ownerIndex = m_ownerIndex[uint(msg.sender)]; - // make sure they're an owner - if (ownerIndex == 0) return; - uint ownerIndexBit = 2**ownerIndex; - var pending = m_pending[_operation]; - if (pending.ownersDone & ownerIndexBit > 0) { - pending.yetNeeded++; - pending.ownersDone -= ownerIndexBit; - Revoke(msg.sender, _operation); - } - } - - // Replaces an owner `_from` with another `_to`. - function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) { - if (isOwner(_to)) return; - uint ownerIndex = m_ownerIndex[uint(_from)]; - if (ownerIndex == 0) return; - - clearPending(); - m_owners[ownerIndex] = uint(_to); - m_ownerIndex[uint(_from)] = 0; - m_ownerIndex[uint(_to)] = ownerIndex; - OwnerChanged(_from, _to); - } - - function addOwner(address _owner) onlymanyowners(sha3(msg.data)) { - if (isOwner(_owner)) return; - - clearPending(); - if (m_numOwners >= c_maxOwners) - reorganizeOwners(); - if (m_numOwners >= c_maxOwners) - return; - m_numOwners++; - m_owners[m_numOwners] = uint(_owner); - m_ownerIndex[uint(_owner)] = m_numOwners; - OwnerAdded(_owner); - } - - function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) { - uint ownerIndex = m_ownerIndex[uint(_owner)]; - if (ownerIndex == 0) return; - if (m_required > m_numOwners - 1) return; - - m_owners[ownerIndex] = 0; - m_ownerIndex[uint(_owner)] = 0; - clearPending(); - reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot - OwnerRemoved(_owner); - } - - function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) { - if (_newRequired > m_numOwners) return; - m_required = _newRequired; - clearPending(); - RequirementChanged(_newRequired); - } - - function isOwner(address _addr) returns (bool) { - return m_ownerIndex[uint(_addr)] > 0; - } - - - function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { - var pending = m_pending[_operation]; - uint ownerIndex = m_ownerIndex[uint(_owner)]; - - // make sure they're an owner - if (ownerIndex == 0) return false; - - // determine the bit to set for this owner. - uint ownerIndexBit = 2**ownerIndex; - return !(pending.ownersDone & ownerIndexBit == 0); - } - - // INTERNAL METHODS - - function confirmAndCheck(bytes32 _operation) internal returns (bool) { - // determine what index the present sender is: - uint ownerIndex = m_ownerIndex[uint(msg.sender)]; - // make sure they're an owner - if (ownerIndex == 0) return; - - var pending = m_pending[_operation]; - // if we're not yet working on this operation, switch over and reset the confirmation status. - if (pending.yetNeeded == 0) { - // reset count of confirmations needed. - pending.yetNeeded = m_required; - // reset which owners have confirmed (none) - set our bitmap to 0. - pending.ownersDone = 0; - pending.index = m_pendingIndex.length++; - m_pendingIndex[pending.index] = _operation; - } - // determine the bit to set for this owner. - uint ownerIndexBit = 2**ownerIndex; - // make sure we (the message sender) haven't confirmed this operation previously. - if (pending.ownersDone & ownerIndexBit == 0) { - Confirmation(msg.sender, _operation); - // ok - check if count is enough to go ahead. - if (pending.yetNeeded <= 1) { - // enough confirmations: reset and run interior. - delete m_pendingIndex[m_pending[_operation].index]; - delete m_pending[_operation]; - return true; - } - else - { - // not enough: record that this owner in particular confirmed. - pending.yetNeeded--; - pending.ownersDone |= ownerIndexBit; - } - } - } - - function reorganizeOwners() private { - uint free = 1; - while (free < m_numOwners) - { - while (free < m_numOwners && m_owners[free] != 0) free++; - while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; - if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) - { - m_owners[free] = m_owners[m_numOwners]; - m_ownerIndex[m_owners[free]] = free; - m_owners[m_numOwners] = 0; - } - } - } - - function clearPending() internal { - uint length = m_pendingIndex.length; - for (uint i = 0; i < length; ++i) - if (m_pendingIndex[i] != 0) - delete m_pending[m_pendingIndex[i]]; - delete m_pendingIndex; - } - - - /****************************** - ****** DAY LIMIT SECTION ***** - ******************************/ - - // MODIFIERS - - // simple modifier for daily limit. - modifier limitedDaily(uint _value) { - if (underLimit(_value)) - _; - } - - // METHODS - - // constructor - stores initial daily limit and records the present day's index. - function initDaylimit(uint _limit) { - m_dailyLimit = _limit; - m_lastDay = today(); - } - // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. - function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) { - m_dailyLimit = _newLimit; - } - // resets the amount already spent today. needs many of the owners to confirm. - function resetSpentToday() onlymanyowners(sha3(msg.data)) { - m_spentToday = 0; - } - - // INTERNAL METHODS - - // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and - // returns true. otherwise just returns false. - function underLimit(uint _value) internal onlyowner returns (bool) { - // reset the spend limit if we're on a different day to last time. - if (today() > m_lastDay) { - m_spentToday = 0; - m_lastDay = today(); - } - // check to see if there's enough left - if so, subtract and return true. - // overflow protection // dailyLimit check - if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { - m_spentToday += _value; - return true; - } - return false; - } - - // determines today's index. - function today() private constant returns (uint) { return now / 1 days; } - - - /****************************** - ********* WALLET SECTION ***** - ******************************/ - - // METHODS - - // constructor - just pass on the owner array to the multiowned and - // the limit to daylimit - function initWallet(address[] _owners, uint _required, uint _daylimit) { - initMultiowned(_owners, _required); - initDaylimit(_daylimit) ; - } - - // kills the contract sending everything to `_to`. - function kill(address _to) onlymanyowners(sha3(msg.data)) { - suicide(_to); - } - - // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. - // If not, goes into multisig process. We provide a hash on return to allow the sender to provide - // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value - // and _data arguments). They still get the option of using them if they want, anyways. - function execute(address _to, uint _value, bytes _data) onlyowner returns(bool _callValue) { - // first, take the opportunity to check that we're under the daily limit. - if (underLimit(_value)) { - SingleTransact(msg.sender, _value, _to, _data); - // yes - just execute the call. - _callValue =_to.call.value(_value)(_data); - } else { - // determine our operation hash. - bytes32 _r = sha3(msg.data, block.number); - if (!confirm(_r) && m_txs[_r].to == 0) { - m_txs[_r].to = _to; - m_txs[_r].value = _value; - m_txs[_r].data = _data; - ConfirmationNeeded(_r, msg.sender, _value, _to, _data); - } - } - } - - // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order - // to determine the body of the transaction from the hash provided. - function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { - if (m_txs[_h].to != 0) { - m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); - MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); - delete m_txs[_h]; - return true; - } - } - - // INTERNAL METHODS - - function clearWalletPending() internal { - uint length = m_pendingIndex.length; - for (uint i = 0; i < length; ++i) - delete m_txs[m_pendingIndex[i]]; - clearPending(); - } - - // FIELDS - address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; - - // the number of owners that must confirm the same operation before it is run. - uint m_required; - // pointer used to find a free slot in m_owners - uint m_numOwners; - - uint public m_dailyLimit; - uint public m_spentToday; - uint public m_lastDay; - - // list of owners - uint[256] m_owners; - uint constant c_maxOwners = 250; - - // index on the list of owners to allow reverse lookup - mapping(uint => uint) m_ownerIndex; - // the ongoing operations. - mapping(bytes32 => PendingState) m_pending; - bytes32[] m_pendingIndex; - - // pending transactions we have at present. - mapping (bytes32 => Transaction) m_txs; + // pending transactions we have at present. + mapping (bytes32 => Transaction) m_txs; } +contract Wallet is WalletEvents { -contract Wallet is multisig { + // WALLET CONSTRUCTOR + // calls the `initWallet` method of the Library in this context + function Wallet(address[] _owners, uint _required, uint _daylimit) { + // Signature of the Wallet Library's init function + bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)")); + address target = _walletLibrary; - // WALLET CONSTRUCTOR - // calls the `initWallet` method of the Library in this context - function Wallet(address[] _owners, uint _required, uint _daylimit) { - // Signature of the Wallet Library's init function - bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)")); - address target = _walletLibrary; + // Compute the size of the call data : arrays has 2 + // 32bytes for offset and length, plus 32bytes per element ; + // plus 2 32bytes for each uint + uint argarraysize = (2 + _owners.length); + uint argsize = (2 + argarraysize) * 32; - // Compute the size of the call data : arrays has 2 - // 32bytes for offset and length, plus 32bytes per element ; - // plus 2 32bytes for each uint - uint argarraysize = (2 + _owners.length); - uint argsize = (2 + argarraysize) * 32; - - assembly { - // Add the signature first to memory - mstore(0x0, sig) - // Add the call data, which is at the end of the - // code - codecopy(0x4, sub(codesize, argsize), argsize) - // Delegate call to the library - delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0) - } + assembly { + // Add the signature first to memory + mstore(0x0, sig) + // Add the call data, which is at the end of the + // code + codecopy(0x4, sub(codesize, argsize), argsize) + // Delegate call to the library + delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0) } + } - // METHODS + // METHODS - // gets called when no other function matches - function() payable { - // just being sent some cash? - if (msg.value > 0) - Deposit(msg.sender, msg.value); - else if (msg.data.length > 0) - _walletLibrary.delegatecall(msg.data); - } + // gets called when no other function matches + function() payable { + // just being sent some cash? + if (msg.value > 0) + Deposit(msg.sender, msg.value); + else if (msg.data.length > 0) + _walletLibrary.delegatecall(msg.data); + } - // Gets an owner by 0-indexed position (using numOwners as the count) - function getOwner(uint ownerIndex) constant returns (address) { - return address(m_owners[ownerIndex + 1]); - } + // Gets an owner by 0-indexed position (using numOwners as the count) + function getOwner(uint ownerIndex) constant returns (address) { + return address(m_owners[ownerIndex + 1]); + } - // As return statement unavailable in fallback, explicit the method here + // As return statement unavailable in fallback, explicit the method here - function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { - return _walletLibrary.delegatecall(msg.data); - } + function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) { + return _walletLibrary.delegatecall(msg.data); + } - function isOwner(address _addr) returns (bool) { - return _walletLibrary.delegatecall(msg.data); - } + function isOwner(address _addr) constant returns (bool) { + return _walletLibrary.delegatecall(msg.data); + } - // FIELDS - address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; + // FIELDS + address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; - // the number of owners that must confirm the same operation before it is run. - uint public m_required; - // pointer used to find a free slot in m_owners - uint public m_numOwners; + // the number of owners that must confirm the same operation before it is run. + uint public m_required; + // pointer used to find a free slot in m_owners + uint public m_numOwners; - uint public m_dailyLimit; - uint public m_spentToday; - uint public m_lastDay; + uint public m_dailyLimit; + uint public m_spentToday; + uint public m_lastDay; - // list of owners - uint[256] m_owners; + // list of owners + uint[256] m_owners; } diff --git a/js/src/contracts/snippets/wallet.sol b/js/src/contracts/snippets/wallet.sol index b369eea76..18de9df68 100644 --- a/js/src/contracts/snippets/wallet.sol +++ b/js/src/contracts/snippets/wallet.sol @@ -8,221 +8,222 @@ // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the // interior is executed. -pragma solidity ^0.4.6; + +pragma solidity ^0.4.9; contract multiowned { - // TYPES + // TYPES - // struct for the status of a pending operation. - struct PendingState { - uint yetNeeded; - uint ownersDone; - uint index; + // struct for the status of a pending operation. + struct PendingState { + uint yetNeeded; + uint ownersDone; + uint index; + } + + // EVENTS + + // this contract only has six types of events: it can accept a confirmation, in which case + // we record owner and operation (hash) alongside it. + event Confirmation(address owner, bytes32 operation); + event Revoke(address owner, bytes32 operation); + // some others are in the case of an owner changing. + event OwnerChanged(address oldOwner, address newOwner); + event OwnerAdded(address newOwner); + event OwnerRemoved(address oldOwner); + // the last one is emitted if the required signatures change + event RequirementChanged(uint newRequirement); + + // MODIFIERS + + // simple single-sig function modifier. + modifier onlyowner { + if (isOwner(msg.sender)) + _; + } + // multi-sig function modifier: the operation must have an intrinsic hash in order + // that later attempts can be realised as the same underlying operation and + // thus count as confirmations. + modifier onlymanyowners(bytes32 _operation) { + if (confirmAndCheck(_operation)) + _; + } + + // METHODS + + // constructor is given number of sigs required to do protected "onlymanyowners" transactions + // as well as the selection of addresses capable of confirming them. + function multiowned(address[] _owners, uint _required) { + m_numOwners = _owners.length + 1; + m_owners[1] = uint(msg.sender); + m_ownerIndex[uint(msg.sender)] = 1; + for (uint i = 0; i < _owners.length; ++i) + { + m_owners[2 + i] = uint(_owners[i]); + m_ownerIndex[uint(_owners[i])] = 2 + i; } + m_required = _required; + } - // EVENTS - - // this contract only has six types of events: it can accept a confirmation, in which case - // we record owner and operation (hash) alongside it. - event Confirmation(address owner, bytes32 operation); - event Revoke(address owner, bytes32 operation); - // some others are in the case of an owner changing. - event OwnerChanged(address oldOwner, address newOwner); - event OwnerAdded(address newOwner); - event OwnerRemoved(address oldOwner); - // the last one is emitted if the required signatures change - event RequirementChanged(uint newRequirement); - - // MODIFIERS - - // simple single-sig function modifier. - modifier onlyowner { - if (isOwner(msg.sender)) - _; + // Revokes a prior confirmation of the given operation + function revoke(bytes32 _operation) external { + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + uint ownerIndexBit = 2**ownerIndex; + var pending = m_pending[_operation]; + if (pending.ownersDone & ownerIndexBit > 0) { + pending.yetNeeded++; + pending.ownersDone -= ownerIndexBit; + Revoke(msg.sender, _operation); } - // multi-sig function modifier: the operation must have an intrinsic hash in order - // that later attempts can be realised as the same underlying operation and - // thus count as confirmations. - modifier onlymanyowners(bytes32 _operation) { - if (confirmAndCheck(_operation)) - _; + } + + // Replaces an owner `_from` with another `_to`. + function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_to)) return; + uint ownerIndex = m_ownerIndex[uint(_from)]; + if (ownerIndex == 0) return; + + clearPending(); + m_owners[ownerIndex] = uint(_to); + m_ownerIndex[uint(_from)] = 0; + m_ownerIndex[uint(_to)] = ownerIndex; + OwnerChanged(_from, _to); + } + + function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_owner)) return; + + clearPending(); + if (m_numOwners >= c_maxOwners) + reorganizeOwners(); + if (m_numOwners >= c_maxOwners) + return; + m_numOwners++; + m_owners[m_numOwners] = uint(_owner); + m_ownerIndex[uint(_owner)] = m_numOwners; + OwnerAdded(_owner); + } + + function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + uint ownerIndex = m_ownerIndex[uint(_owner)]; + if (ownerIndex == 0) return; + if (m_required > m_numOwners - 1) return; + + m_owners[ownerIndex] = 0; + m_ownerIndex[uint(_owner)] = 0; + clearPending(); + reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot + OwnerRemoved(_owner); + } + + function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { + if (_newRequired > m_numOwners) return; + m_required = _newRequired; + clearPending(); + RequirementChanged(_newRequired); + } + + // Gets an owner by 0-indexed position (using numOwners as the count) + function getOwner(uint ownerIndex) external constant returns (address) { + return address(m_owners[ownerIndex + 1]); + } + + function isOwner(address _addr) constant returns (bool) { + return m_ownerIndex[uint(_addr)] > 0; + } + + function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { + var pending = m_pending[_operation]; + uint ownerIndex = m_ownerIndex[uint(_owner)]; + + // make sure they're an owner + if (ownerIndex == 0) return false; + + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + return !(pending.ownersDone & ownerIndexBit == 0); + } + + // INTERNAL METHODS + + function confirmAndCheck(bytes32 _operation) internal returns (bool) { + // determine what index the present sender is: + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + + var pending = m_pending[_operation]; + // if we're not yet working on this operation, switch over and reset the confirmation status. + if (pending.yetNeeded == 0) { + // reset count of confirmations needed. + pending.yetNeeded = m_required; + // reset which owners have confirmed (none) - set our bitmap to 0. + pending.ownersDone = 0; + pending.index = m_pendingIndex.length++; + m_pendingIndex[pending.index] = _operation; } - - // METHODS - - // constructor is given number of sigs required to do protected "onlymanyowners" transactions - // as well as the selection of addresses capable of confirming them. - function multiowned(address[] _owners, uint _required) { - m_numOwners = _owners.length + 1; - m_owners[1] = uint(msg.sender); - m_ownerIndex[uint(msg.sender)] = 1; - for (uint i = 0; i < _owners.length; ++i) - { - m_owners[2 + i] = uint(_owners[i]); - m_ownerIndex[uint(_owners[i])] = 2 + i; - } - m_required = _required; + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + // make sure we (the message sender) haven't confirmed this operation previously. + if (pending.ownersDone & ownerIndexBit == 0) { + Confirmation(msg.sender, _operation); + // ok - check if count is enough to go ahead. + if (pending.yetNeeded <= 1) { + // enough confirmations: reset and run interior. + delete m_pendingIndex[m_pending[_operation].index]; + delete m_pending[_operation]; + return true; + } + else + { + // not enough: record that this owner in particular confirmed. + pending.yetNeeded--; + pending.ownersDone |= ownerIndexBit; + } } + } - // Revokes a prior confirmation of the given operation - function revoke(bytes32 _operation) external { - uint ownerIndex = m_ownerIndex[uint(msg.sender)]; - // make sure they're an owner - if (ownerIndex == 0) return; - uint ownerIndexBit = 2**ownerIndex; - var pending = m_pending[_operation]; - if (pending.ownersDone & ownerIndexBit > 0) { - pending.yetNeeded++; - pending.ownersDone -= ownerIndexBit; - Revoke(msg.sender, _operation); - } + function reorganizeOwners() private { + uint free = 1; + while (free < m_numOwners) + { + while (free < m_numOwners && m_owners[free] != 0) free++; + while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; + if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) + { + m_owners[free] = m_owners[m_numOwners]; + m_ownerIndex[m_owners[free]] = free; + m_owners[m_numOwners] = 0; + } } + } - // Replaces an owner `_from` with another `_to`. - function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { - if (isOwner(_to)) return; - uint ownerIndex = m_ownerIndex[uint(_from)]; - if (ownerIndex == 0) return; + function clearPending() internal { + uint length = m_pendingIndex.length; + for (uint i = 0; i < length; ++i) + if (m_pendingIndex[i] != 0) + delete m_pending[m_pendingIndex[i]]; + delete m_pendingIndex; + } - clearPending(); - m_owners[ownerIndex] = uint(_to); - m_ownerIndex[uint(_from)] = 0; - m_ownerIndex[uint(_to)] = ownerIndex; - OwnerChanged(_from, _to); - } + // FIELDS - function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { - if (isOwner(_owner)) return; + // the number of owners that must confirm the same operation before it is run. + uint public m_required; + // pointer used to find a free slot in m_owners + uint public m_numOwners; - clearPending(); - if (m_numOwners >= c_maxOwners) - reorganizeOwners(); - if (m_numOwners >= c_maxOwners) - return; - m_numOwners++; - m_owners[m_numOwners] = uint(_owner); - m_ownerIndex[uint(_owner)] = m_numOwners; - OwnerAdded(_owner); - } - - function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { - uint ownerIndex = m_ownerIndex[uint(_owner)]; - if (ownerIndex == 0) return; - if (m_required > m_numOwners - 1) return; - - m_owners[ownerIndex] = 0; - m_ownerIndex[uint(_owner)] = 0; - clearPending(); - reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot - OwnerRemoved(_owner); - } - - function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { - if (_newRequired > m_numOwners) return; - m_required = _newRequired; - clearPending(); - RequirementChanged(_newRequired); - } - - // Gets an owner by 0-indexed position (using numOwners as the count) - function getOwner(uint ownerIndex) external constant returns (address) { - return address(m_owners[ownerIndex + 1]); - } - - function isOwner(address _addr) returns (bool) { - return m_ownerIndex[uint(_addr)] > 0; - } - - function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { - var pending = m_pending[_operation]; - uint ownerIndex = m_ownerIndex[uint(_owner)]; - - // make sure they're an owner - if (ownerIndex == 0) return false; - - // determine the bit to set for this owner. - uint ownerIndexBit = 2**ownerIndex; - return !(pending.ownersDone & ownerIndexBit == 0); - } - - // INTERNAL METHODS - - function confirmAndCheck(bytes32 _operation) internal returns (bool) { - // determine what index the present sender is: - uint ownerIndex = m_ownerIndex[uint(msg.sender)]; - // make sure they're an owner - if (ownerIndex == 0) return; - - var pending = m_pending[_operation]; - // if we're not yet working on this operation, switch over and reset the confirmation status. - if (pending.yetNeeded == 0) { - // reset count of confirmations needed. - pending.yetNeeded = m_required; - // reset which owners have confirmed (none) - set our bitmap to 0. - pending.ownersDone = 0; - pending.index = m_pendingIndex.length++; - m_pendingIndex[pending.index] = _operation; - } - // determine the bit to set for this owner. - uint ownerIndexBit = 2**ownerIndex; - // make sure we (the message sender) haven't confirmed this operation previously. - if (pending.ownersDone & ownerIndexBit == 0) { - Confirmation(msg.sender, _operation); - // ok - check if count is enough to go ahead. - if (pending.yetNeeded <= 1) { - // enough confirmations: reset and run interior. - delete m_pendingIndex[m_pending[_operation].index]; - delete m_pending[_operation]; - return true; - } - else - { - // not enough: record that this owner in particular confirmed. - pending.yetNeeded--; - pending.ownersDone |= ownerIndexBit; - } - } - } - - function reorganizeOwners() private { - uint free = 1; - while (free < m_numOwners) - { - while (free < m_numOwners && m_owners[free] != 0) free++; - while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; - if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) - { - m_owners[free] = m_owners[m_numOwners]; - m_ownerIndex[m_owners[free]] = free; - m_owners[m_numOwners] = 0; - } - } - } - - function clearPending() internal { - uint length = m_pendingIndex.length; - for (uint i = 0; i < length; ++i) - if (m_pendingIndex[i] != 0) - delete m_pending[m_pendingIndex[i]]; - delete m_pendingIndex; - } - - // FIELDS - - // the number of owners that must confirm the same operation before it is run. - uint public m_required; - // pointer used to find a free slot in m_owners - uint public m_numOwners; - - // list of owners - uint[256] m_owners; - uint constant c_maxOwners = 250; - // index on the list of owners to allow reverse lookup - mapping(uint => uint) m_ownerIndex; - // the ongoing operations. - mapping(bytes32 => PendingState) m_pending; - bytes32[] m_pendingIndex; + // list of owners + uint[256] m_owners; + uint constant c_maxOwners = 250; + // index on the list of owners to allow reverse lookup + mapping(uint => uint) m_ownerIndex; + // the ongoing operations. + mapping(bytes32 => PendingState) m_pending; + bytes32[] m_pendingIndex; } // inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable) @@ -230,79 +231,70 @@ contract multiowned { // uses is specified in the modifier. contract daylimit is multiowned { - // MODIFIERS + // METHODS - // simple modifier for daily limit. - modifier limitedDaily(uint _value) { - if (underLimit(_value)) - _; + // constructor - stores initial daily limit and records the present day's index. + function daylimit(uint _limit) { + m_dailyLimit = _limit; + m_lastDay = today(); + } + // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. + function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { + m_dailyLimit = _newLimit; + } + // resets the amount already spent today. needs many of the owners to confirm. + function resetSpentToday() onlymanyowners(sha3(msg.data)) external { + m_spentToday = 0; + } + + // INTERNAL METHODS + + // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and + // returns true. otherwise just returns false. + function underLimit(uint _value) internal onlyowner returns (bool) { + // reset the spend limit if we're on a different day to last time. + if (today() > m_lastDay) { + m_spentToday = 0; + m_lastDay = today(); } - - // METHODS - - // constructor - stores initial daily limit and records the present day's index. - function daylimit(uint _limit) { - m_dailyLimit = _limit; - m_lastDay = today(); - } - // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. - function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { - m_dailyLimit = _newLimit; - } - // resets the amount already spent today. needs many of the owners to confirm. - function resetSpentToday() onlymanyowners(sha3(msg.data)) external { - m_spentToday = 0; + // check to see if there's enough left - if so, subtract and return true. + // overflow protection // dailyLimit check + if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { + m_spentToday += _value; + return true; } + return false; + } + // determines today's index. + function today() private constant returns (uint) { return now / 1 days; } - // INTERNAL METHODS + // FIELDS - // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and - // returns true. otherwise just returns false. - function underLimit(uint _value) internal onlyowner returns (bool) { - // reset the spend limit if we're on a different day to last time. - if (today() > m_lastDay) { - m_spentToday = 0; - m_lastDay = today(); - } - // check to see if there's enough left - if so, subtract and return true. - // overflow protection // dailyLimit check - if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { - m_spentToday += _value; - return true; - } - return false; - } - // determines today's index. - function today() private constant returns (uint) { return now / 1 days; } - - // FIELDS - - uint public m_dailyLimit; - uint public m_spentToday; - uint public m_lastDay; + uint public m_dailyLimit; + uint public m_spentToday; + uint public m_lastDay; } // interface contract for multisig proxy contracts; see below for docs. contract multisig { - // EVENTS + // EVENTS - // logged events: - // Funds has arrived into the wallet (record how much). - event Deposit(address _from, uint value); - // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). - event SingleTransact(address owner, uint value, address to, bytes data); - // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). - event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); - // Confirmation still needed for a transaction. - event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); + // logged events: + // Funds has arrived into the wallet (record how much). + event Deposit(address _from, uint value); + // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). + event SingleTransact(address owner, uint value, address to, bytes data, address created); + // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). + event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created); + // Confirmation still needed for a transaction. + event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); - // FUNCTIONS + // FUNCTIONS - // TODO: document - function changeOwner(address _from, address _to) external; - function execute(address _to, uint _value, bytes _data) external returns (bytes32); - function confirm(bytes32 _h) returns (bool); + // TODO: document + function execute(address _to, uint _value, bytes _data) external returns (bytes32 o_hash); + function confirm(bytes32 _h) external returns (bool o_success); } // usage: @@ -310,79 +302,102 @@ contract multisig { // Wallet(w).from(anotherOwner).confirm(h); contract Wallet is multisig, multiowned, daylimit { - // TYPES + // TYPES - // Transaction structure to remember details of transaction lest it need be saved for a later call. - struct Transaction { - address to; - uint value; - bytes data; + // Transaction structure to remember details of transaction lest it need be saved for a later call. + struct Transaction { + address to; + uint value; + bytes data; + } + + // METHODS + + // constructor - just pass on the owner array to the multiowned and + // the limit to daylimit + function Wallet(address[] _owners, uint _required, uint _daylimit) + multiowned(_owners, _required) daylimit(_daylimit) { + } + + // kills the contract sending everything to `_to`. + function kill(address _to) onlymanyowners(sha3(msg.data)) external { + suicide(_to); + } + + // gets called when no other function matches + function() payable { + // just being sent some cash? + if (msg.value > 0) + Deposit(msg.sender, msg.value); + } + + // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. + // If not, goes into multisig process. We provide a hash on return to allow the sender to provide + // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value + // and _data arguments). They still get the option of using them if they want, anyways. + function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) { + // first, take the opportunity to check that we're under the daily limit. + if ((_data.length == 0 && underLimit(_value)) || m_required == 1) { + // yes - just execute the call. + address created; + if (_to == 0) { + created = create(_value, _data); + } else { + if (!_to.call.value(_value)(_data)) + throw; + } + SingleTransact(msg.sender, _value, _to, _data, created); + } else { + // determine our operation hash. + o_hash = sha3(msg.data, block.number); + // store if it's new + if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) { + m_txs[o_hash].to = _to; + m_txs[o_hash].value = _value; + m_txs[o_hash].data = _data; + } + if (!confirm(o_hash)) { + ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data); + } } + } - // METHODS - - // constructor - just pass on the owner array to the multiowned and - // the limit to daylimit - function Wallet(address[] _owners, uint _required, uint _daylimit) - multiowned(_owners, _required) daylimit(_daylimit) { + function create(uint _value, bytes _code) internal returns (address o_addr) { + assembly { + o_addr := create(_value, add(_code, 0x20), mload(_code)) + jumpi(invalidJumpLabel, iszero(extcodesize(o_addr))) } + } - // kills the contract sending everything to `_to`. - function kill(address _to) onlymanyowners(sha3(msg.data)) external { - suicide(_to); + // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order + // to determine the body of the transaction from the hash provided. + function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) { + if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) { + address created; + if (m_txs[_h].to == 0) { + created = create(m_txs[_h].value, m_txs[_h].data); + } else { + if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data)) + throw; + } + + MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created); + delete m_txs[_h]; + return true; } + } - // gets called when no other function matches - function() payable { - // just being sent some cash? - if (msg.value > 0) - Deposit(msg.sender, msg.value); - } + // INTERNAL METHODS - // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. - // If not, goes into multisig process. We provide a hash on return to allow the sender to provide - // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value - // and _data arguments). They still get the option of using them if they want, anyways. - function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) { - // first, take the opportunity to check that we're under the daily limit. - if (underLimit(_value)) { - SingleTransact(msg.sender, _value, _to, _data); - // yes - just execute the call. - _to.call.value(_value)(_data); - return 0; - } - // determine our operation hash. - _r = sha3(msg.data, block.number); - if (!confirm(_r) && m_txs[_r].to == 0) { - m_txs[_r].to = _to; - m_txs[_r].value = _value; - m_txs[_r].data = _data; - ConfirmationNeeded(_r, msg.sender, _value, _to, _data); - } - } + function clearPending() internal { + uint length = m_pendingIndex.length; + for (uint i = 0; i < length; ++i) + delete m_txs[m_pendingIndex[i]]; + super.clearPending(); + } - // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order - // to determine the body of the transaction from the hash provided. - function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { - if (m_txs[_h].to != 0) { - m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); - MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); - delete m_txs[_h]; - return true; - } - } + // FIELDS - // INTERNAL METHODS - - function clearPending() internal { - uint length = m_pendingIndex.length; - for (uint i = 0; i < length; ++i) - delete m_txs[m_pendingIndex[i]]; - super.clearPending(); - } - - // FIELDS - - // pending transactions we have at present. - mapping (bytes32 => Transaction) m_txs; + // pending transactions we have at present. + mapping (bytes32 => Transaction) m_txs; } diff --git a/js/src/modals/CreateWallet/WalletDetails/walletDetails.js b/js/src/modals/CreateWallet/WalletDetails/walletDetails.js index 6b0ed7e1a..d3776e2cb 100644 --- a/js/src/modals/CreateWallet/WalletDetails/walletDetails.js +++ b/js/src/modals/CreateWallet/WalletDetails/walletDetails.js @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -import { omitBy } from 'lodash'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -106,9 +105,6 @@ export default class WalletDetails extends Component { renderMultisigDetails () { const { accounts, wallet, errors } = this.props; - // Wallets cannot create contracts - const _accounts = omitBy(accounts, (a) => a.wallet); - return ( <Form> <Input @@ -148,7 +144,7 @@ export default class WalletDetails extends Component { /> <AddressSelect - accounts={ _accounts } + accounts={ accounts } error={ errors.account } hint={ <FormattedMessage diff --git a/js/src/modals/CreateWallet/createWalletStore.js b/js/src/modals/CreateWallet/createWalletStore.js index 213c35a36..e9d854270 100644 --- a/js/src/modals/CreateWallet/createWalletStore.js +++ b/js/src/modals/CreateWallet/createWalletStore.js @@ -22,10 +22,11 @@ import Contract from '~/api/contract'; import { ERROR_CODES } from '~/api/transport/error'; import Contracts from '~/contracts'; import { wallet as walletAbi } from '~/contracts/abi'; -import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; +import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { validateUint, validateAddress, validateName } from '~/util/validation'; import { toWei } from '~/api/util/wei'; +import { deploy } from '~/util/tx'; import WalletsUtils from '~/util/wallets'; const STEPS = { @@ -179,6 +180,8 @@ export default class CreateWalletStore { this.wallet.owners = owners; this.wallet.required = require.toNumber(); this.wallet.dailylimit = dailylimit.limit; + + this.wallet = this.getWalletWithMeta(this.wallet); }); return this.addWallet(this.wallet); @@ -202,21 +205,51 @@ export default class CreateWalletStore { return null; // exception when registry is not available }) .then((address) => { - const walletLibraryAddress = (address || '').replace(/^0x/, '').toLowerCase(); - const code = walletLibraryAddress.length && !/^0+$/.test(walletLibraryAddress) - ? walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress) - : fullWalletCode; + if (!address || /^(0x)?0*$/.test(address)) { + return null; + } + + // Check that it's actually the expected code + return this.api.eth + .getCode(address) + .then((code) => { + const strippedCode = code.replace(/^0x/, ''); + + // The actual deployed code is included in the wallet + // library code (which might have some more data) + if (walletLibraryCode.indexOf(strippedCode) >= 0) { + return address; + } + + return null; + }); + }) + .then((address) => { + let code = fullWalletCode; + + if (address) { + const walletLibraryAddress = address.replace(/^0x/, '').toLowerCase(); + + code = walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress); + } else { + console.warn('wallet library has not been found in the registry'); + } const options = { data: code, from: account }; - return this.api - .newContract(walletAbi) - .deploy(options, [ owners, required, daylimit ], this.onDeploymentState); + const contract = this.api.newContract(walletAbi); + + this.wallet = this.getWalletWithMeta(this.wallet); + return deploy(contract, options, [ owners, required, daylimit ], this.wallet.metadata, this.onDeploymentState); }) .then((address) => { + if (!address || /^(0x)?0*$/.test(address)) { + return false; + } + this.deployed = true; this.wallet.address = address; return this.addWallet(this.wallet); @@ -233,26 +266,37 @@ export default class CreateWalletStore { } @action addWallet = (wallet) => { - const { address, name, description } = wallet; + const { address, name, metadata } = wallet; return Promise .all([ this.api.parity.setAccountName(address, name), - this.api.parity.setAccountMeta(address, { - abi: walletAbi, - wallet: true, - timestamp: Date.now(), - deleted: false, - description, - name, - tags: ['wallet'] - }) + this.api.parity.setAccountMeta(address, metadata) ]) .then(() => { this.step = 'INFO'; }); } + getWalletWithMeta = (wallet) => { + const { name, description } = wallet; + + const metadata = { + abi: walletAbi, + wallet: true, + timestamp: Date.now(), + deleted: false, + tags: [ 'wallet' ], + description, + name + }; + + return { + ...wallet, + metadata + }; + } + onDeploymentState = (error, data) => { if (error) { return console.error('createWallet::onDeploymentState', error); @@ -298,6 +342,15 @@ export default class CreateWalletStore { ); return; + case 'confirmationNeeded': + this.deployState = ( + <FormattedMessage + id='createWallet.states.confirmationNeeded' + defaultMessage='The contract deployment needs confirmations from other owners of the Wallet' + /> + ); + return; + case 'completed': this.deployState = ( <FormattedMessage diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 9337b1430..14930d312 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -import { pick, omitBy } from 'lodash'; +import { pick } from 'lodash'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -23,6 +23,7 @@ import { connect } from 'react-redux'; import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui'; import { CancelIcon, DoneIcon } from '~/ui/Icons'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; +import { deploy, deployEstimateGas } from '~/util/tx'; import DetailsStep from './DetailsStep'; import ParametersStep from './ParametersStep'; @@ -73,7 +74,7 @@ class DeployContract extends Component { static contextTypes = { api: PropTypes.object.isRequired, store: PropTypes.object.isRequired - } + }; static propTypes = { accounts: PropTypes.object.isRequired, @@ -422,9 +423,9 @@ class DeployContract extends Component { from: fromAddress }; - api - .newContract(abiParsed) - .deployEstimateGas(options, params) + const contract = api.newContract(abiParsed); + + deployEstimateGas(contract, options, params) .then(([gasEst, gas]) => { this.gasStore.setEstimated(gasEst.toFixed(0)); this.gasStore.setGas(gas.toFixed(0)); @@ -490,6 +491,17 @@ class DeployContract extends Component { const { api, store } = this.context; const { source } = this.props; const { abiParsed, code, description, name, params, fromAddress } = this.state; + + const metadata = { + abi: abiParsed, + contract: true, + deleted: false, + timestamp: Date.now(), + name, + description, + source + }; + const options = { data: code, from: fromAddress @@ -499,28 +511,25 @@ class DeployContract extends Component { const contract = api.newContract(abiParsed); - contract - .deploy(options, params, this.onDeploymentState) + deploy(contract, options, params, metadata, this.onDeploymentState) .then((address) => { - const blockNumber = contract._receipt + // No contract address given, might need some confirmations + // from the wallet owners... + if (!address || /^(0x)?0*$/.test(address)) { + return false; + } + + metadata.blockNumber = contract._receipt ? contract.receipt.blockNumber.toNumber() : null; return Promise.all([ api.parity.setAccountName(address, name), - api.parity.setAccountMeta(address, { - abi: abiParsed, - contract: true, - timestamp: Date.now(), - deleted: false, - blockNumber, - description, - source - }) + api.parity.setAccountMeta(address, metadata) ]) .then(() => { console.log(`contract deployed at ${address}`); - this.setState({ step: 'DEPLOYMENT', address }); + this.setState({ step: 'COMPLETED', address }); }); }) .catch((error) => { @@ -589,6 +598,17 @@ class DeployContract extends Component { }); return; + case 'confirmationNeeded': + this.setState({ + deployState: ( + <FormattedMessage + id='deployContract.state.confirmationNeeded' + defaultMessage='The operation needs confirmations from the other owners of the contract' + /> + ) + }); + return; + case 'completed': this.setState({ deployState: ( @@ -614,17 +634,14 @@ class DeployContract extends Component { function mapStateToProps (initState, initProps) { const { accounts } = initProps; - // Skip Wallet accounts : they can't create Contracts - const _accounts = omitBy(accounts, (a) => a.wallet); - - const fromAddresses = Object.keys(_accounts); + const fromAddresses = Object.keys(accounts); return (state) => { const balances = pick(state.balances.balances, fromAddresses); const { gasLimit } = state.nodeStatus; return { - accounts: _accounts, + accounts, balances, gasLimit }; diff --git a/js/src/modals/WalletSettings/walletSettings.js b/js/src/modals/WalletSettings/walletSettings.js index 9f787b0e3..7ba13ea0c 100644 --- a/js/src/modals/WalletSettings/walletSettings.js +++ b/js/src/modals/WalletSettings/walletSettings.js @@ -401,6 +401,7 @@ class WalletSettings extends Component { const cancelBtn = ( <Button icon={ <CancelIcon /> } + key='cancelBtn' label={ <FormattedMessage id='walletSettings.buttons.cancel' @@ -414,6 +415,7 @@ class WalletSettings extends Component { const closeBtn = ( <Button icon={ <CancelIcon /> } + key='closeBtn' label={ <FormattedMessage id='walletSettings.buttons.close' @@ -427,6 +429,7 @@ class WalletSettings extends Component { const sendingBtn = ( <Button icon={ <DoneIcon /> } + key='sendingBtn' label={ <FormattedMessage id='walletSettings.buttons.sending' @@ -440,6 +443,7 @@ class WalletSettings extends Component { const nextBtn = ( <Button icon={ <NextIcon /> } + key='nextBtn' label={ <FormattedMessage id='walletSettings.buttons.next' @@ -454,6 +458,7 @@ class WalletSettings extends Component { const sendBtn = ( <Button icon={ <NextIcon /> } + key='sendBtn' label={ <FormattedMessage id='walletSettings.buttons.send' diff --git a/js/src/redux/providers/personal.js b/js/src/redux/providers/personal.js index 61ef23cbf..ffc331495 100644 --- a/js/src/redux/providers/personal.js +++ b/js/src/redux/providers/personal.js @@ -49,7 +49,7 @@ export default class Personal { .filter((address) => { const account = accountsInfo[address]; - return !account.uuid && account.meta.deleted; + return !account.uuid && account.meta && account.meta.deleted; }) .map((address) => this._api.parity.removeAddress(address)) ); diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js index 01ba13b53..edafbef31 100644 --- a/js/src/redux/providers/personalActions.js +++ b/js/src/redux/providers/personalActions.js @@ -26,7 +26,6 @@ import WalletsUtils from '~/util/wallets'; import { wallet as WalletAbi } from '~/contracts/abi'; export function personalAccountsInfo (accountsInfo) { - const addresses = []; const accounts = {}; const contacts = {}; const contracts = {}; @@ -35,10 +34,9 @@ export function personalAccountsInfo (accountsInfo) { Object.keys(accountsInfo || {}) .map((address) => Object.assign({}, accountsInfo[address], { address })) - .filter((account) => account.uuid || !account.meta.deleted) + .filter((account) => account.meta && (account.uuid || !account.meta.deleted)) .forEach((account) => { if (account.uuid) { - addresses.push(account.address); accounts[account.address] = account; } else if (account.meta.wallet) { account.wallet = true; @@ -87,18 +85,45 @@ export function personalAccountsInfo (accountsInfo) { return []; }) .then((_wallets) => { - _wallets.forEach((wallet) => { - const owners = wallet.owners.map((o) => o.address); + // We want to separate owned wallets and other wallets + // However, wallets can be owned by wallets, that can + // be owned by an account... + let otherWallets = [].concat(_wallets); + let prevLength; + let nextLength; - // Owners ∩ Addresses not null : Wallet is owned - // by one of the accounts - if (intersection(owners, addresses).length > 0) { - accounts[wallet.address] = wallet; - } else { - contacts[wallet.address] = wallet; - } + // If no more other wallets, or if the size decreased, continue... + do { + prevLength = otherWallets.length; + + otherWallets = otherWallets + .map((wallet) => { + const addresses = Object.keys(accounts); + const owners = wallet.owners.map((o) => o.address); + + // Owners ∩ Addresses not null : Wallet is owned + // by one of the accounts + if (intersection(owners, addresses).length > 0) { + accounts[wallet.address] = wallet; + return false; + } + + return wallet; + }) + .filter((wallet) => wallet); + + nextLength = otherWallets.length; + } while (nextLength < prevLength); + + // And other wallets to contacts... + otherWallets.forEach((wallet) => { + contacts[wallet.address] = wallet; }); + // Cache the _real_ accounts for + // WalletsUtils (used for sending transactions) + WalletsUtils.cacheAccounts(accounts); + dispatch(_personalAccountsInfo({ accountsInfo, accounts, diff --git a/js/src/redux/providers/walletActions.js b/js/src/redux/providers/walletActions.js index a37f04d15..b31a2b35b 100644 --- a/js/src/redux/providers/walletActions.js +++ b/js/src/redux/providers/walletActions.js @@ -431,22 +431,7 @@ function parseLogs (logs) { return; } - const { wallet } = getState(); - const { contract } = wallet; - const walletInstance = contract.instance; - - const signatures = { - OwnerChanged: toHex(walletInstance.OwnerChanged.signature), - OwnerAdded: toHex(walletInstance.OwnerAdded.signature), - OwnerRemoved: toHex(walletInstance.OwnerRemoved.signature), - RequirementChanged: toHex(walletInstance.RequirementChanged.signature), - Confirmation: toHex(walletInstance.Confirmation.signature), - Revoke: toHex(walletInstance.Revoke.signature), - Deposit: toHex(walletInstance.Deposit.signature), - SingleTransact: toHex(walletInstance.SingleTransact.signature), - MultiTransact: toHex(walletInstance.MultiTransact.signature), - ConfirmationNeeded: toHex(walletInstance.ConfirmationNeeded.signature) - }; + const WalletSignatures = WalletsUtils.getWalletSignatures(); const updates = {}; @@ -459,25 +444,25 @@ function parseLogs (logs) { }; switch (eventSignature) { - case signatures.OwnerChanged: - case signatures.OwnerAdded: - case signatures.OwnerRemoved: + case WalletSignatures.OwnerChanged: + case WalletSignatures.OwnerAdded: + case WalletSignatures.OwnerRemoved: updates[address] = { ...prev, [ UPDATE_OWNERS ]: true }; return; - case signatures.RequirementChanged: + case WalletSignatures.RequirementChanged: updates[address] = { ...prev, [ UPDATE_REQUIRE ]: true }; return; - case signatures.ConfirmationNeeded: - case signatures.Confirmation: - case signatures.Revoke: + case WalletSignatures.ConfirmationNeeded: + case WalletSignatures.Confirmation: + case WalletSignatures.Revoke: const operation = bytesToHex(log.params.operation.value); updates[address] = { @@ -489,9 +474,11 @@ function parseLogs (logs) { return; - case signatures.Deposit: - case signatures.SingleTransact: - case signatures.MultiTransact: + case WalletSignatures.Deposit: + case WalletSignatures.SingleTransact: + case WalletSignatures.MultiTransact: + case WalletSignatures.Old.SingleTransact: + case WalletSignatures.Old.MultiTransact: updates[address] = { ...prev, [ UPDATE_TRANSACTIONS ]: true diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 25f031c62..87edb7779 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -120,6 +120,18 @@ class MethodDecoding extends Component { <span className={ styles.highlight }> { gas.toFormat(0) } gas ({ gasPrice.div(1000000).toFormat(0) }M/<small>ETH</small>) </span> + { + transaction.gasUsed + ? ( + <span> + <span>used</span> + <span className={ styles.highlight }> + { transaction.gasUsed.toFormat(0) } gas + </span> + </span> + ) + : null + } <span> for a total transaction value of </span> <span className={ styles.highlight }>{ this.renderEtherValue(gasValue) }</span> { this.renderMinBlock() } diff --git a/js/src/ui/TxList/store.js b/js/src/ui/TxList/store.js index 1a3c2a2ff..1e670e31d 100644 --- a/js/src/ui/TxList/store.js +++ b/js/src/ui/TxList/store.js @@ -51,6 +51,7 @@ export default class Store { return bnB.comparedTo(bnA); }); + this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0)); }); } @@ -85,26 +86,53 @@ export default class Store { this._subscriptionId = 0; } - loadTransactions (_txhashes) { - const txhashes = _txhashes.filter((hash) => !this.transactions[hash] || this._pendingHashes.includes(hash)); + loadTransactions (_txhashes = []) { + const promises = _txhashes + .filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash)) + .map((txhash) => { + return Promise + .all([ + this._api.eth.getTransactionByHash(txhash), + this._api.eth.getTransactionReceipt(txhash) + ]) + .then(([ + transaction = {}, + transactionReceipt = {} + ]) => { + return { + ...transactionReceipt, + ...transaction + }; + }); + }); - if (!txhashes || !txhashes.length) { + if (!promises.length) { return; } Promise - .all(txhashes.map((txhash) => this._api.eth.getTransactionByHash(txhash))) + .all(promises) .then((_transactions) => { - const transactions = _transactions.filter((tx) => tx); + const blockNumbers = []; + const transactions = _transactions + .filter((tx) => tx && tx.hash) + .reduce((txs, tx) => { + txs[tx.hash] = tx; - this.addTransactions( - transactions.reduce((transactions, tx, index) => { - transactions[txhashes[index]] = tx; - return transactions; - }, {}) - ); + if (tx.blockNumber && tx.blockNumber.gt(0)) { + blockNumbers.push(tx.blockNumber.toNumber()); + } - this.loadBlocks(transactions.map((tx) => tx.blockNumber ? tx.blockNumber.toNumber() : 0)); + return txs; + }, {}); + + // No need to add transactions if there are none + if (Object.keys(transactions).length === 0) { + return false; + } + + this.addTransactions(transactions); + this.loadBlocks(blockNumbers); }) .catch((error) => { console.warn('loadTransactions', error); diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js index f3043a47f..c2224903f 100644 --- a/js/src/ui/TxList/txList.js +++ b/js/src/ui/TxList/txList.js @@ -27,7 +27,7 @@ import styles from './txList.css'; class TxList extends Component { static contextTypes = { api: PropTypes.object.isRequired - } + }; static propTypes = { address: PropTypes.string.isRequired, @@ -36,7 +36,7 @@ class TxList extends Component { PropTypes.object ]).isRequired, netVersion: PropTypes.string.isRequired - } + }; store = new Store(this.context.api); diff --git a/js/src/util/tx.js b/js/src/util/tx.js index 49ffee3da..8a48a3d26 100644 --- a/js/src/util/tx.js +++ b/js/src/util/tx.js @@ -61,17 +61,6 @@ export function estimateGas (_func, _options, _values = []) { const { func, options, values } = callArgs; return func._estimateGas(options, values); - }) - .then((gas) => { - return WalletsUtils - .isWallet(_func.contract.api, _options.from) - .then((isWallet) => { - if (isWallet) { - return gas.mul(1.5); - } - - return gas; - }); }); } @@ -84,6 +73,114 @@ export function postTransaction (_func, _options, _values = []) { }); } +export function deploy (contract, _options, values, metadata = {}, statecb = () => {}) { + const options = { ..._options }; + const { api } = contract; + const address = options.from; + + return WalletsUtils + .isWallet(api, address) + .then((isWallet) => { + if (!isWallet) { + return contract.deploy(options, values, statecb); + } + + statecb(null, { state: 'estimateGas' }); + + return deployEstimateGas(contract, options, values) + .then(([gasEst, gas]) => { + options.gas = gas.toFixed(0); + + statecb(null, { state: 'postTransaction', gas }); + + return WalletsUtils.getDeployArgs(contract, options, values); + }) + .then((callArgs) => { + const { func, options, values } = callArgs; + + return func._postTransaction(options, values) + .then((requestId) => { + statecb(null, { state: 'checkRequest', requestId }); + return contract._pollCheckRequest(requestId); + }) + .then((txhash) => { + statecb(null, { state: 'getTransactionReceipt', txhash }); + return contract._pollTransactionReceipt(txhash, options.gas); + }) + .then((receipt) => { + if (receipt.gasUsed.eq(options.gas)) { + throw new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`); + } + + const logs = WalletsUtils.parseLogs(api, receipt.logs || []); + + const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded'); + const transactionLog = logs.find((log) => log.event === 'SingleTransact'); + + if (!confirmationLog && !transactionLog) { + throw new Error('Something went wrong in the Wallet Contract (no logs have been emitted)...'); + } + + // Confirmations are needed from the other owners + if (confirmationLog) { + const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value); + + // Add the contract to pending contracts + WalletsUtils.addPendingContract(address, operationHash, metadata); + statecb(null, { state: 'confirmationNeeded' }); + return; + } + + // Set the contract address in the receip + receipt.contractAddress = transactionLog.params.created.value; + + const contractAddress = receipt.contractAddress; + + statecb(null, { state: 'hasReceipt', receipt }); + contract._receipt = receipt; + contract._address = contractAddress; + + statecb(null, { state: 'getCode' }); + + return api.eth.getCode(contractAddress) + .then((code) => { + if (code === '0x') { + throw new Error('Contract not deployed, getCode returned 0x'); + } + + statecb(null, { state: 'completed' }); + return contractAddress; + }); + }); + }); + }); +} + +export function deployEstimateGas (contract, _options, values) { + const options = { ..._options }; + const { api } = contract; + const address = options.from; + + return WalletsUtils + .isWallet(api, address) + .then((isWallet) => { + if (!isWallet) { + return contract.deployEstimateGas(options, values); + } + + return WalletsUtils + .getDeployArgs(contract, options, values) + .then((callArgs) => { + const { func, options, values } = callArgs; + + return func.estimateGas(options, values); + }) + .then((gasEst) => { + return [gasEst, gasEst.mul(1.05)]; + }); + }); +} + export function patchApi (api) { api.patch = { ...api.patch, diff --git a/js/src/util/wallets.js b/js/src/util/wallets.js index 3bc86f6ed..904f77900 100644 --- a/js/src/util/wallets.js +++ b/js/src/util/wallets.js @@ -16,59 +16,154 @@ import BigNumber from 'bignumber.js'; import { intersection, range, uniq } from 'lodash'; +import store from 'store'; +import Abi from '~/abi'; import Contract from '~/api/contract'; import { bytesToHex, toHex } from '~/api/util/format'; import { validateAddress } from '~/util/validation'; import WalletAbi from '~/contracts/abi/wallet.json'; +import OldWalletAbi from '~/contracts/abi/old-wallet.json'; + +const LS_PENDING_CONTRACTS_KEY = '_parity::wallets::pendingContracts'; const _cachedWalletLookup = {}; +let _cachedAccounts = {}; + +const walletAbi = new Abi(WalletAbi); +const oldWalletAbi = new Abi(OldWalletAbi); + +const walletEvents = walletAbi.events.reduce((events, event) => { + events[event.name] = event; + return events; +}, {}); + +const oldWalletEvents = oldWalletAbi.events.reduce((events, event) => { + events[event.name] = event; + return events; +}, {}); + +const WalletSignatures = { + OwnerChanged: toHex(walletEvents.OwnerChanged.signature), + OwnerAdded: toHex(walletEvents.OwnerAdded.signature), + OwnerRemoved: toHex(walletEvents.OwnerRemoved.signature), + RequirementChanged: toHex(walletEvents.RequirementChanged.signature), + Confirmation: toHex(walletEvents.Confirmation.signature), + Revoke: toHex(walletEvents.Revoke.signature), + Deposit: toHex(walletEvents.Deposit.signature), + SingleTransact: toHex(walletEvents.SingleTransact.signature), + MultiTransact: toHex(walletEvents.MultiTransact.signature), + ConfirmationNeeded: toHex(walletEvents.ConfirmationNeeded.signature), + + Old: { + SingleTransact: toHex(oldWalletEvents.SingleTransact.signature), + MultiTransact: toHex(oldWalletEvents.MultiTransact.signature) + } +}; export default class WalletsUtils { + static getWalletSignatures () { + return WalletSignatures; + } + + static getPendingContracts () { + return store.get(LS_PENDING_CONTRACTS_KEY) || {}; + } + + static setPendingContracts (contracts = {}) { + return store.set(LS_PENDING_CONTRACTS_KEY, contracts); + } + + static removePendingContract (operationHash) { + const nextContracts = WalletsUtils.getPendingContracts(); + + delete nextContracts[operationHash]; + WalletsUtils.setPendingContracts(nextContracts); + } + + static addPendingContract (address, operationHash, metadata) { + const nextContracts = { + ...WalletsUtils.getPendingContracts(), + [ operationHash ]: { + address, + metadata, + operationHash + } + }; + + WalletsUtils.setPendingContracts(nextContracts); + } + + static cacheAccounts (accounts) { + _cachedAccounts = accounts; + } + static getCallArgs (api, options, values = []) { const walletContract = new Contract(api, WalletAbi); + const walletAddress = options.from; - const promises = [ - api.parity.accountsInfo(), - WalletsUtils.fetchOwners(walletContract.at(options.from)) - ]; + return WalletsUtils + .fetchOwners(walletContract.at(walletAddress)) + .then((owners) => { + const addresses = Object.keys(_cachedAccounts); + const ownerAddress = intersection(addresses, owners).pop(); - return Promise - .all(promises) - .then(([ accounts, owners ]) => { - const addresses = Object.keys(accounts); - const owner = intersection(addresses, owners).pop(); - - if (!owner) { + if (!ownerAddress) { return false; } - return owner; - }) - .then((owner) => { - if (!owner) { - return false; - } - - const _options = Object.assign({}, options); - const { from, to, value = new BigNumber(0), data } = options; + const account = _cachedAccounts[ownerAddress]; + const _options = { ...options }; + const { to, value = new BigNumber(0), data } = _options; delete _options.data; const nextValues = [ to, value, data ]; const nextOptions = { ..._options, - from: owner, - to: from, + from: ownerAddress, + to: walletAddress, value: new BigNumber(0) }; const execFunc = walletContract.instance.execute; + const callArgs = { func: execFunc, options: nextOptions, values: nextValues }; - return { func: execFunc, options: nextOptions, values: nextValues }; + if (!account.wallet) { + return callArgs; + } + + const nextData = walletContract.getCallData(execFunc, nextOptions, nextValues); + + return WalletsUtils.getCallArgs(api, { ...nextOptions, data: nextData }, nextValues); }); } + static getDeployArgs (contract, options, values) { + const { api } = contract; + const func = contract.constructors[0]; + + options.data = contract.getCallData(func, options, values); + options.to = '0x'; + + return WalletsUtils + .getCallArgs(api, options, values) + .then((callArgs) => { + if (!callArgs) { + console.error('no call args', callArgs); + throw new Error('you do not own this wallet'); + } + + return callArgs; + }); + } + + static parseLogs (api, logs = []) { + const walletContract = new Contract(api, WalletAbi); + + return walletContract.parseEventLogs(logs); + } + /** * Check whether the given address could be * a Wallet. The result is cached in order not @@ -199,16 +294,18 @@ export default class WalletsUtils { } static fetchTransactions (walletContract) { - const walletInstance = walletContract.instance; - const signatures = { - single: toHex(walletInstance.SingleTransact.signature), - multi: toHex(walletInstance.MultiTransact.signature), - deposit: toHex(walletInstance.Deposit.signature) - }; + const { api } = walletContract; + const pendingContracts = WalletsUtils.getPendingContracts(); return walletContract .getAllLogs({ - topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ] + topics: [ [ + WalletSignatures.SingleTransact, + WalletSignatures.MultiTransact, + WalletSignatures.Deposit, + WalletSignatures.Old.SingleTransact, + WalletSignatures.Old.MultiTransact + ] ] }) .then((logs) => { return logs.sort((logA, logB) => { @@ -226,11 +323,11 @@ export default class WalletsUtils { const signature = toHex(log.topics[0]); const value = log.params.value.value; - const from = signature === signatures.deposit + const from = signature === WalletSignatures.Deposit ? log.params['_from'].value : walletContract.address; - const to = signature === signatures.deposit + const to = signature === WalletSignatures.Deposit ? walletContract.address : log.params.to.value; @@ -240,8 +337,53 @@ export default class WalletsUtils { from, to, value }; + if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) { + transaction.creates = log.params.created.value; + delete transaction.to; + } + if (log.params.operation) { - transaction.operation = bytesToHex(log.params.operation.value); + const operation = bytesToHex(log.params.operation.value); + + // Add the pending contract to the contracts + if (pendingContracts[operation]) { + const { metadata } = pendingContracts[operation]; + const contractName = metadata.name; + + metadata.blockNumber = log.blockNumber; + + // The contract creation might not be in the same log, + // but must be in the same transaction (eg. Contract creation + // from Wallet within a Wallet) + api.eth + .getTransactionReceipt(log.transactionHash) + .then((transactionReceipt) => { + const transactionLogs = WalletsUtils.parseLogs(api, transactionReceipt.logs); + const creationLog = transactionLogs.find((log) => { + return log.params.created && !/^(0x)?0*$/.test(log.params.created.value); + }); + + if (!creationLog) { + return false; + } + + const contractAddress = creationLog.params.created.value; + + return Promise + .all([ + api.parity.setAccountName(contractAddress, contractName), + api.parity.setAccountMeta(contractAddress, metadata) + ]) + .then(() => { + WalletsUtils.removePendingContract(operation); + }); + }) + .catch((error) => { + console.error('adding wallet contract', error); + }); + } + + transaction.operation = operation; } if (log.params.data) { diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index ad50bad87..76e3522f8 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -41,8 +41,9 @@ export default class SignerStore { this.balances = Object.assign({}, this.balances, balances); } - @action setLocalHashes = (localHashes) => { - if (!isEqual(localHashes, this.localHashes)) { + @action setLocalHashes = (localHashes = []) => { + // Use slice to make sure they are both Arrays (MobX uses Objects for Observable Arrays) + if (!isEqual(localHashes.slice(), this.localHashes.slice())) { this.localHashes = localHashes; } } diff --git a/js/src/views/Wallet/Transactions/transactions.js b/js/src/views/Wallet/Transactions/transactions.js index 0ef853a70..aa1fae672 100644 --- a/js/src/views/Wallet/Transactions/transactions.js +++ b/js/src/views/Wallet/Transactions/transactions.js @@ -71,7 +71,7 @@ export default class WalletTransactions extends Component { } const txRows = transactions.slice(0, 15).map((transaction, index) => { - const { transactionHash, blockNumber, from, to, value, data } = transaction; + const { transactionHash, data } = transaction; return ( <TxRow @@ -79,12 +79,9 @@ export default class WalletTransactions extends Component { netVersion={ netVersion } key={ `${transactionHash}_${index}` } tx={ { - blockNumber, - from, hash: transactionHash, input: data && bytesToHex(data) || '', - to, - value + ...transaction } } /> ); From c3c83086bc579611075577769c37a0e18a87c44f Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 7 Mar 2017 20:21:07 +0100 Subject: [PATCH 78/93] SMS Faucet (#4774) * Faucet * Remove flakey button-index testing * Only display faucet when sms verified (mainnet) * simplify availability checks * WIP * Resuest from verified -> verified * Update endpoint, display response text * Error icon on errors * Parse hash text response * Use /api/:address endpoint * hash -> data * Adjust sms-certified message --- js/src/modals/Faucet/faucet.js | 162 +++++++++++++++++++++++++++ js/src/modals/Faucet/index.js | 17 +++ js/src/modals/Faucet/store.js | 126 +++++++++++++++++++++ js/src/modals/index.js | 1 + js/src/ui/Icons/index.js | 1 + js/src/ui/ModalBox/modalBox.js | 22 +++- js/src/views/Account/account.js | 97 +++++++++++++--- js/src/views/Account/account.spec.js | 41 ------- js/src/views/Account/store.js | 5 + js/src/views/Account/store.spec.js | 8 ++ 10 files changed, 421 insertions(+), 59 deletions(-) create mode 100644 js/src/modals/Faucet/faucet.js create mode 100644 js/src/modals/Faucet/index.js create mode 100644 js/src/modals/Faucet/store.js diff --git a/js/src/modals/Faucet/faucet.js b/js/src/modals/Faucet/faucet.js new file mode 100644 index 000000000..e4399e8ba --- /dev/null +++ b/js/src/modals/Faucet/faucet.js @@ -0,0 +1,162 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import { observer } from 'mobx-react'; +import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { txLink } from '~/3rdparty/etherscan/links'; +import { Button, ModalBox, Portal, ShortenedHash } from '~/ui'; +import { CloseIcon, DialIcon, DoneIcon, ErrorIcon, SendIcon } from '~/ui/Icons'; + +import Store from './store'; + +@observer +export default class Faucet extends Component { + static propTypes = { + address: PropTypes.string.isRequired, + netVersion: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired + } + + store = new Store(this.props.netVersion, this.props.address); + + render () { + const { error, isBusy, isCompleted } = this.store; + + let icon = <DialIcon />; + + if (isCompleted) { + icon = error + ? <ErrorIcon /> + : <DoneIcon />; + } + + return ( + <Portal + buttons={ this.renderActions() } + busy={ isBusy } + isSmallModal + onClose={ this.onClose } + open + title={ + <FormattedMessage + id='faucet.title' + defaultMessage='Kovan ETH Faucet' + /> + } + > + <ModalBox + icon={ icon } + summary={ + isCompleted + ? this.renderSummaryDone() + : this.renderSummaryRequest() + } + /> + </Portal> + ); + } + + renderActions = () => { + const { canTransact, isBusy, isCompleted } = this.store; + + return isCompleted || isBusy + ? ( + <Button + disabled={ isBusy } + icon={ <DoneIcon /> } + key='done' + label={ + <FormattedMessage + id='faucet.buttons.done' + defaultMessage='close' + /> + } + onClick={ this.onClose } + /> + ) + : [ + <Button + icon={ <CloseIcon /> } + key='close' + label={ + <FormattedMessage + id='faucet.buttons.close' + defaultMessage='close' + /> + } + onClick={ this.onClose } + />, + <Button + disabled={ !canTransact } + icon={ <SendIcon /> } + key='request' + label={ + <FormattedMessage + id='faucet.buttons.request' + defaultMessage='request' + /> + } + onClick={ this.onExecute } + /> + ]; + } + + renderSummaryDone () { + const { error, responseText, responseTxHash } = this.store; + + return ( + <div> + <FormattedMessage + id='faucet.summary.done' + defaultMessage='Your Kovan ETH has been requested from the faucet which responded with -' + /> + { + error + ? ( + <p>{ error }</p> + ) + : ( + <p> + <span>{ responseText } </span> + <a href={ txLink(responseTxHash, false, '42') } target='_blank'> + <ShortenedHash data={ responseTxHash } /> + </a> + </p> + ) + } + </div> + ); + } + + renderSummaryRequest () { + return ( + <FormattedMessage + id='faucet.summary.info' + defaultMessage='To request a deposit of Kovan ETH to this address, you need to ensure that the address is sms-verified on the mainnet. Once executed the faucet will deposit Kovan ETH into the current account.' + /> + ); + } + + onClose = () => { + this.props.onClose(); + } + + onExecute = () => { + return this.store.makeItRain(); + } +} diff --git a/js/src/modals/Faucet/index.js b/js/src/modals/Faucet/index.js new file mode 100644 index 000000000..9aaa695dc --- /dev/null +++ b/js/src/modals/Faucet/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +export default from './faucet'; diff --git a/js/src/modals/Faucet/store.js b/js/src/modals/Faucet/store.js new file mode 100644 index 000000000..356a4c080 --- /dev/null +++ b/js/src/modals/Faucet/store.js @@ -0,0 +1,126 @@ +// Copyright 2015-2017 Parity Technologies (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 <http://www.gnu.org/licenses/>. + +import { action, computed, observable, transaction } from 'mobx'; +import apiutil from '~/api/util'; + +const ENDPOINT = 'http://faucet.kovan.network/api/'; + +export default class Store { + @observable addressReceive = null; + @observable addressVerified = null; + @observable error = null; + @observable responseText = null; + @observable responseTxHash = null; + @observable isBusy = false; + @observable isCompleted = false; + @observable isDestination = false; + @observable isDone = false; + + constructor (netVersion, address) { + transaction(() => { + this.setDestination(netVersion === '42'); + + this.setAddressReceive(address); + this.setAddressVerified(address); + }); + } + + @computed get canTransact () { + return !this.isBusy && this.addressReceiveValid && this.addressVerifiedValid; + } + + @computed get addressReceiveValid () { + return apiutil.isAddressValid(this.addressReceive); + } + + @computed get addressVerifiedValid () { + return apiutil.isAddressValid(this.addressVerified); + } + + @action setAddressReceive = (address) => { + this.addressReceive = address; + } + + @action setAddressVerified = (address) => { + this.addressVerified = address; + } + + @action setBusy = (isBusy) => { + this.isBusy = isBusy; + } + + @action setCompleted = (isCompleted) => { + transaction(() => { + this.setBusy(false); + this.isCompleted = isCompleted; + }); + } + + @action setDestination = (isDestination) => { + this.isDestination = isDestination; + } + + @action setError = (error) => { + if (error.indexOf('not certified') !== -1) { + this.error = `${error}. Please ensure that this account is sms certified on the mainnet.`; + } else { + this.error = error; + } + } + + @action setResponse = (response) => { + this.responseText = response.result; + this.responseTxHash = response.tx; + } + + makeItRain = () => { + this.setBusy(true); + + const options = { + method: 'GET', + mode: 'cors' + }; + const url = `${ENDPOINT}${this.addressVerified}`; + + return fetch(url, options) + .then((response) => { + if (!response.ok) { + return null; + } + + return response.json(); + }) + .catch(() => { + return null; + }) + .then((response) => { + transaction(() => { + if (!response || response.error) { + this.setError( + response + ? response.error + : 'Unable to complete request to the faucet, the server may be unavailable. Please try again later.' + ); + } else { + this.setResponse(response); + } + + this.setCompleted(true); + }); + }); + } +} diff --git a/js/src/modals/index.js b/js/src/modals/index.js index e64c90dce..6fe10bfb1 100644 --- a/js/src/modals/index.js +++ b/js/src/modals/index.js @@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount'; export DeployContract from './DeployContract'; export EditMeta from './EditMeta'; export ExecuteContract from './ExecuteContract'; +export Faucet from './Faucet'; export FirstRun from './FirstRun'; export LoadContract from './LoadContract'; export PasswordManager from './PasswordManager'; diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index ff3e86b26..aae962a88 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -31,6 +31,7 @@ export DashboardIcon from 'material-ui/svg-icons/action/dashboard'; export DeleteIcon from 'material-ui/svg-icons/action/delete'; export DevelopIcon from 'material-ui/svg-icons/action/description'; export DoneIcon from 'material-ui/svg-icons/action/done-all'; +export DialIcon from 'material-ui/svg-icons/communication/dialpad'; export EditIcon from 'material-ui/svg-icons/content/create'; export ErrorIcon from 'material-ui/svg-icons/alert/error'; export FileUploadIcon from 'material-ui/svg-icons/file/file-upload'; diff --git a/js/src/ui/ModalBox/modalBox.js b/js/src/ui/ModalBox/modalBox.js index f0366cf06..d74dc1c6e 100644 --- a/js/src/ui/ModalBox/modalBox.js +++ b/js/src/ui/ModalBox/modalBox.js @@ -22,13 +22,13 @@ import styles from './modalBox.css'; export default class ModalBox extends Component { static propTypes = { - children: PropTypes.node.isRequired, + children: PropTypes.node, icon: PropTypes.node.isRequired, summary: nodeOrStringProptype() } render () { - const { children, icon } = this.props; + const { icon } = this.props; return ( <div className={ styles.body }> @@ -37,14 +37,26 @@ export default class ModalBox extends Component { </div> <div className={ styles.content }> { this.renderSummary() } - <div className={ styles.body }> - { children } - </div> + { this.renderBody() } </div> </div> ); } + renderBody () { + const { children } = this.props; + + if (!children) { + return null; + } + + return ( + <div className={ styles.body }> + { children } + </div> + ); + } + renderSummary () { const { summary } = this.props; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index 197bda591..c2b600be8 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -22,11 +22,11 @@ import { bindActionCreators } from 'redux'; import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; import HardwareStore from '~/mobx/hardwareStore'; -import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals'; +import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals'; import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; import { Actionbar, Button, Page } from '~/ui'; -import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons'; +import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons'; import DeleteAddress from '../Address/Delete'; @@ -48,6 +48,8 @@ class Account extends Component { accounts: PropTypes.object, balances: PropTypes.object, + certifications: PropTypes.object, + netVersion: PropTypes.string.isRequired, params: PropTypes.object } @@ -97,6 +99,7 @@ class Account extends Component { <div> { this.renderDeleteDialog(account) } { this.renderEditDialog(account) } + { this.renderFaucetDialog() } { this.renderFundDialog() } { this.renderPasswordDialog(account) } { this.renderTransferDialog(account, balance) } @@ -117,8 +120,35 @@ class Account extends Component { ); } + isKovan = (netVersion) => { + return netVersion === '42'; + } + + isMainnet = (netVersion) => { + return netVersion === '1'; + } + + isFaucettable = (netVersion, certifications, address) => { + return this.isKovan(netVersion) || ( + this.isMainnet(netVersion) && + this.isSmsCertified(certifications, address) + ); + } + + isSmsCertified = (_certifications, address) => { + const certifications = _certifications && _certifications[address] + ? _certifications[address].filter((cert) => cert.name.indexOf('smsverification') === 0) + : []; + + return certifications.length !== 0; + } + renderActionbar (account, balance) { + const { certifications, netVersion } = this.props; + const { address } = this.props.params; const showTransferButton = !!(balance && balance.tokens); + const isVerifiable = this.isMainnet(netVersion); + const isFaucettable = this.isFaucettable(netVersion, certifications, address); const buttons = [ <Button @@ -149,17 +179,36 @@ class Account extends Component { } onClick={ this.store.toggleFundDialog } />, - <Button - icon={ <VerifyIcon /> } - key='sms-verification' - label={ - <FormattedMessage - id='account.button.verify' - defaultMessage='verify' + isVerifiable + ? ( + <Button + icon={ <VerifyIcon /> } + key='verification' + label={ + <FormattedMessage + id='account.button.verify' + defaultMessage='verify' + /> + } + onClick={ this.store.toggleVerificationDialog } /> - } - onClick={ this.store.toggleVerificationDialog } - />, + ) + : null, + isFaucettable + ? ( + <Button + icon={ <DialIcon /> } + key='faucet' + label={ + <FormattedMessage + id='account.button.faucet' + defaultMessage='Kovan ETH' + /> + } + onClick={ this.store.toggleFaucetDialog } + /> + ) + : null, <Button icon={ <EditIcon /> } key='editmeta' @@ -253,6 +302,24 @@ class Account extends Component { ); } + renderFaucetDialog () { + const { netVersion } = this.props; + + if (!this.store.isFaucetVisible) { + return null; + } + + const { address } = this.props.params; + + return ( + <Faucet + address={ address } + netVersion={ netVersion } + onClose={ this.store.toggleFaucetDialog } + /> + ); + } + renderFundDialog () { if (!this.store.isFundVisible) { return null; @@ -317,10 +384,14 @@ class Account extends Component { function mapStateToProps (state) { const { accounts } = state.personal; const { balances } = state.balances; + const certifications = state.certifications; + const { netVersion } = state.nodeStatus; return { accounts, - balances + balances, + certifications, + netVersion }; } diff --git a/js/src/views/Account/account.spec.js b/js/src/views/Account/account.spec.js index 932e30719..6a96d6300 100644 --- a/js/src/views/Account/account.spec.js +++ b/js/src/views/Account/account.spec.js @@ -80,57 +80,16 @@ describe('views/Account', () => { describe('sub-renderers', () => { describe('renderActionBar', () => { let bar; - let barShallow; beforeEach(() => { render(); bar = instance.renderActionbar({ tokens: {} }); - barShallow = shallow(bar); }); it('renders the bar', () => { expect(bar.type).to.match(/Actionbar/); }); - - // TODO: Finding by index is not optimal, however couldn't find a better method atm - // since we cannot find by key (prop not visible in shallow debug()) - describe('clicks', () => { - it('toggles transfer on click', () => { - barShallow.find('Button').at(0).simulate('click'); - expect(store.isTransferVisible).to.be.true; - }); - - it('toggles fund on click', () => { - barShallow.find('Button').at(1).simulate('click'); - expect(store.isFundVisible).to.be.true; - }); - - it('toggles fund on click', () => { - barShallow.find('Button').at(1).simulate('click'); - expect(store.isFundVisible).to.be.true; - }); - - it('toggles verify on click', () => { - barShallow.find('Button').at(2).simulate('click'); - expect(store.isVerificationVisible).to.be.true; - }); - - it('toggles edit on click', () => { - barShallow.find('Button').at(3).simulate('click'); - expect(store.isEditVisible).to.be.true; - }); - - it('toggles password on click', () => { - barShallow.find('Button').at(4).simulate('click'); - expect(store.isPasswordVisible).to.be.true; - }); - - it('toggles delete on click', () => { - barShallow.find('Button').at(5).simulate('click'); - expect(store.isDeleteVisible).to.be.true; - }); - }); }); describe('renderDeleteDialog', () => { diff --git a/js/src/views/Account/store.js b/js/src/views/Account/store.js index 5b8fe58a0..45a9a7a8c 100644 --- a/js/src/views/Account/store.js +++ b/js/src/views/Account/store.js @@ -19,6 +19,7 @@ import { action, observable } from 'mobx'; export default class Store { @observable isDeleteVisible = false; @observable isEditVisible = false; + @observable isFaucetVisible = false; @observable isFundVisible = false; @observable isPasswordVisible = false; @observable isTransferVisible = false; @@ -32,6 +33,10 @@ export default class Store { this.isEditVisible = !this.isEditVisible; } + @action toggleFaucetDialog = () => { + this.isFaucetVisible = !this.isFaucetVisible; + } + @action toggleFundDialog = () => { this.isFundVisible = !this.isFundVisible; } diff --git a/js/src/views/Account/store.spec.js b/js/src/views/Account/store.spec.js index 408266254..9608b55a3 100644 --- a/js/src/views/Account/store.spec.js +++ b/js/src/views/Account/store.spec.js @@ -31,6 +31,7 @@ describe('views/Account/Store', () => { it('sets all modal visibility to false', () => { expect(store.isDeleteVisible).to.be.false; expect(store.isEditVisible).to.be.false; + expect(store.isFaucetVisible).to.be.false; expect(store.isFundVisible).to.be.false; expect(store.isPasswordVisible).to.be.false; expect(store.isTransferVisible).to.be.false; @@ -53,6 +54,13 @@ describe('views/Account/Store', () => { }); }); + describe('toggleFaucetDialog', () => { + it('toggles the visibility', () => { + store.toggleFaucetDialog(); + expect(store.isFaucetVisible).to.be.true; + }); + }); + describe('toggleFundDialog', () => { it('toggles the visibility', () => { store.toggleFundDialog(); From a587815ddc561a04548608c0efbebada3847ef6b Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Tue, 7 Mar 2017 20:39:36 +0100 Subject: [PATCH 79/93] Re-Introducing HappyPack (#4669) * Updating Dev Dependencies (minor version updates) * Re-introduce HappyPack * Added Yarn Lock file * Use HappyPack * Linting * Delete yarn lock file --- js/.gitignore | 1 + js/package.json | 74 +++++++++++++++--------------- js/src/dapps/tokenreg/Chip/chip.js | 2 +- js/webpack/app.js | 24 +++++----- js/webpack/dev.server.js | 2 +- js/webpack/shared.js | 41 ++++++----------- js/webpack/vendor.js | 3 +- 7 files changed, 68 insertions(+), 79 deletions(-) diff --git a/js/.gitignore b/js/.gitignore index 555c4b4bb..786a10498 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -8,3 +8,4 @@ docs .happypack .npmjs .eslintcache +yarn.lock diff --git a/js/package.json b/js/package.json index 55569372d..8a8d36f60 100644 --- a/js/package.json +++ b/js/package.json @@ -57,28 +57,28 @@ "prepush": "npm run lint:cached" }, "devDependencies": { - "babel-cli": "6.22.2", - "babel-core": "6.22.1", + "babel-cli": "6.23.0", + "babel-core": "6.23.1", "babel-eslint": "7.1.1", - "babel-loader": "6.2.10", + "babel-loader": "6.3.2", "babel-plugin-lodash": "3.2.11", "babel-plugin-react-intl": "2.3.1", "babel-plugin-recharts": "1.1.0", - "babel-plugin-transform-class-properties": "6.22.0", + "babel-plugin-transform-class-properties": "6.23.0", "babel-plugin-transform-decorators-legacy": "1.3.4", - "babel-plugin-transform-object-rest-spread": "6.22.0", - "babel-plugin-transform-react-remove-prop-types": "0.3.0", - "babel-plugin-transform-runtime": "6.22.0", + "babel-plugin-transform-object-rest-spread": "6.23.0", + "babel-plugin-transform-react-remove-prop-types": "0.3.2", + "babel-plugin-transform-runtime": "6.23.0", "babel-plugin-webpack-alias": "2.1.2", - "babel-polyfill": "6.22.0", - "babel-preset-env": "1.1.8", + "babel-polyfill": "6.23.0", + "babel-preset-env": "1.1.9", "babel-preset-es2015": "6.22.0", "babel-preset-es2016": "6.22.0", "babel-preset-es2017": "6.22.0", - "babel-preset-react": "6.22.0", + "babel-preset-react": "6.23.0", "babel-preset-stage-0": "6.22.0", - "babel-register": "6.22.0", - "babel-runtime": "6.22.0", + "babel-register": "6.23.0", + "babel-runtime": "6.23.0", "chai": "3.5.0", "chai-as-promised": "6.0.0", "chai-enzyme": "0.6.1", @@ -86,62 +86,62 @@ "circular-dependency-plugin": "2.0.0", "copy-webpack-plugin": "4.0.1", "core-js": "2.4.1", - "coveralls": "2.11.15", + "coveralls": "2.11.16", "css-loader": "0.26.1", "ejs-loader": "0.3.0", "ejsify": "1.0.0", - "enzyme": "2.7.0", - "eslint": "3.11.1", + "enzyme": "2.7.1", + "eslint": "3.16.1", "eslint-config-semistandard": "7.0.0", "eslint-config-standard": "6.2.1", "eslint-config-standard-react": "4.2.0", - "eslint-plugin-promise": "3.4.0", - "eslint-plugin-react": "6.8.0", + "eslint-plugin-promise": "3.4.2", + "eslint-plugin-react": "6.10.0", "eslint-plugin-standard": "2.0.1", - "express": "4.14.0", + "express": "4.14.1", "extract-loader": "0.1.0", "extract-text-webpack-plugin": "2.0.0-beta.4", - "file-loader": "0.9.0", - "happypack": "3.0.2", + "file-loader": "0.10.0", + "happypack": "3.0.3", "html-loader": "0.4.4", - "html-webpack-plugin": "2.24.1", + "html-webpack-plugin": "2.28.0", "http-proxy-middleware": "0.17.3", - "husky": "0.11.9", + "husky": "0.13.1", "ignore-styles": "5.0.1", - "image-webpack-loader": "3.1.0", + "image-webpack-loader": "3.2.0", "istanbul": "1.0.0-alpha.2", - "jsdom": "9.9.1", + "jsdom": "9.11.0", "json-loader": "0.5.4", "mocha": "3.2.0", "mock-local-storage": "1.0.2", "mock-socket": "6.0.4", - "nock": "9.0.2", - "postcss-import": "9.0.0", - "postcss-loader": "1.2.1", + "nock": "9.0.7", + "postcss-import": "9.1.0", + "postcss-loader": "1.3.2", "postcss-nested": "1.0.0", "postcss-simple-vars": "3.0.0", "progress": "1.1.8", - "progress-bar-webpack-plugin": "1.9.1", + "progress-bar-webpack-plugin": "1.9.3", "raw-loader": "0.5.1", - "react-addons-perf": "15.4.1", - "react-addons-test-utils": "15.4.1", + "react-addons-perf": "15.4.2", + "react-addons-test-utils": "15.4.2", "react-hot-loader": "3.0.0-beta.6", "react-intl-aggregate-webpack-plugin": "0.0.1", "rucksack-css": "0.9.1", - "script-ext-html-webpack-plugin": "1.3.5", - "serviceworker-webpack-plugin": "0.1.7", - "sinon": "1.17.6", + "script-ext-html-webpack-plugin": "1.7.1", + "serviceworker-webpack-plugin": "0.2.0", + "sinon": "1.17.7", "sinon-as-promised": "4.0.2", "sinon-chai": "2.8.0", "style-loader": "0.13.1", - "stylelint": "7.7.0", - "stylelint-config-standard": "15.0.1", + "stylelint": "7.9.0", + "stylelint-config-standard": "16.0.0", "to-source": "2.0.3", "url-loader": "0.5.7", "webpack": "2.2.1", - "webpack-dev-middleware": "1.9.0", + "webpack-dev-middleware": "1.10.1", "webpack-error-notification": "0.1.6", - "webpack-hot-middleware": "2.14.0", + "webpack-hot-middleware": "2.17.1", "websocket": "1.0.24", "yargs": "6.6.0" }, diff --git a/js/src/dapps/tokenreg/Chip/chip.js b/js/src/dapps/tokenreg/Chip/chip.js index 1fa16c774..7c19a671c 100644 --- a/js/src/dapps/tokenreg/Chip/chip.js +++ b/js/src/dapps/tokenreg/Chip/chip.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { Chip } from 'material-ui'; -import IdentityIcon from '../IdentityIcon' ; +import IdentityIcon from '../IdentityIcon'; import styles from './chip.css'; diff --git a/js/webpack/app.js b/js/webpack/app.js index 1e65a181b..14f2876db 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -62,8 +62,7 @@ module.exports = { { test: /\.js$/, exclude: /(node_modules)/, - // use: [ 'happypack/loader?id=js' ] - use: isProd ? 'babel-loader' : 'babel-loader?cacheDirectory=true' + use: [ 'happypack/loader?id=babel-js' ] }, { test: /\.js$/, @@ -96,17 +95,16 @@ module.exports = { test: /\.css$/, include: [ /src/ ], // exclude: [ /src\/dapps/ ], - loader: (isProd && !isEmbed) ? ExtractTextPlugin.extract([ - // 'style-loader', - 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', - 'postcss-loader' - ]) : undefined, - // use: [ 'happypack/loader?id=css' ] - use: (isProd && !isEmbed) ? undefined : [ - 'style-loader', - 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', - 'postcss-loader' - ] + loader: (isProd && !isEmbed) + ? ExtractTextPlugin.extract([ + // 'style-loader', + 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ]) + : undefined, + use: (isProd && !isEmbed) + ? undefined + : [ 'happypack/loader?id=css' ] }, { diff --git a/js/webpack/dev.server.js b/js/webpack/dev.server.js index 2410d3fa8..75ea7703a 100644 --- a/js/webpack/dev.server.js +++ b/js/webpack/dev.server.js @@ -48,7 +48,7 @@ let progressBar = { update: () => {} }; webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); webpackConfig.plugins.push(new webpack.NamedModulesPlugin()); - webpackConfig.plugins.push(new webpack.NoErrorsPlugin()); + webpackConfig.plugins.push(new webpack.NoEmitOnErrorsPlugin()); webpackConfig.plugins.push(new webpack.ProgressPlugin( (percentage) => progressBar.update(percentage) diff --git a/js/webpack/shared.js b/js/webpack/shared.js index 80e79a9d2..3f5fcd66f 100644 --- a/js/webpack/shared.js +++ b/js/webpack/shared.js @@ -17,7 +17,7 @@ const webpack = require('webpack'); const path = require('path'); const fs = require('fs'); -// const HappyPack = require('happypack'); +const HappyPack = require('happypack'); const postcssImport = require('postcss-import'); const postcssNested = require('postcss-nested'); @@ -85,32 +85,21 @@ function getPlugins (_isProd = isProd) { format: '[:msg] [:bar] ' + ':percent' + ' (:elapsed seconds)' }), - // NB: HappyPack is not yet working with Webpack 2... (as of Nov. 26) + new HappyPack({ + id: 'css', + threads: 4, + loaders: [ + 'style-loader', + 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ] + }), - // new HappyPack({ - // id: 'css', - // threads: 4, - // loaders: [ - // 'style-loader', - // 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', - // 'postcss-loader' - // ] - // }), - - // new HappyPack({ - // id: 'js', - // threads: 4, - // loaders: _isProd ? ['babel'] : [ - // 'react-hot-loader', - // 'babel-loader?cacheDirectory=true' - // ] - // }), - - // new HappyPack({ - // id: 'babel', - // threads: 4, - // loaders: ['babel-loader'] - // }), + new HappyPack({ + id: 'babel-js', + threads: 4, + loaders: [ isProd ? 'babel-loader' : 'babel-loader?cacheDirectory=true' ] + }), new webpack.DefinePlugin({ 'process.env': { diff --git a/js/webpack/vendor.js b/js/webpack/vendor.js index b67451305..5081a894f 100644 --- a/js/webpack/vendor.js +++ b/js/webpack/vendor.js @@ -44,7 +44,8 @@ let modules = [ 'recharts', 'redux', 'redux-thunk', - 'scryptsy' + 'scryptsy', + 'zxcvbn' ]; module.exports = { From 7638b2c9e8a0c5bb92321efac6e874729161b01c Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Tue, 7 Mar 2017 20:06:34 +0000 Subject: [PATCH 80/93] [ci skip] js-precompiled 20170307-195934 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ad38ac89..a9bd060a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#94da980fb81d6145e38ca87d37a9137e8440086a" +source = "git+https://github.com/ethcore/js-precompiled.git#190abe2499d7ab2630ded2a127122b5c5a55f956" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 8a8d36f60..bf10c8369 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "1.7.0", + "version": "1.7.1", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 48e5d821867555986ddf3660a88afadd17c82589 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Tue, 7 Mar 2017 22:03:52 +0100 Subject: [PATCH 81/93] Fix SectionList hovering issue (#4749) * Fix SectionList Items hover when <3 items * Even easier... --- js/src/ui/SectionList/sectionList.css | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/js/src/ui/SectionList/sectionList.css b/js/src/ui/SectionList/sectionList.css index c6b3765ff..14e8bffd1 100644 --- a/js/src/ui/SectionList/sectionList.css +++ b/js/src/ui/SectionList/sectionList.css @@ -17,7 +17,6 @@ $transition: all 0.25s; $widthNormal: 33.33%; -$widthShrunk: 29%; $widthExpanded: 42%; .section { @@ -39,18 +38,19 @@ $widthExpanded: 42%; display: flex; justify-content: center; - /* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for */ + /* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for /* case where <> 3 columns are required should the need arrise from a UI pov. */ .item { box-sizing: border-box; display: flex; flex: 0 1 $widthNormal; - max-width: $widthNormal; opacity: 0.85; padding: 0.25em; + /* https://www.binarymoon.co.uk/2014/02/fixing-css-transitions-in-google-chrome/ */ transform: translateZ(0); transition: $transition; + width: 0; &:hover { opacity: 1; @@ -58,22 +58,13 @@ $widthExpanded: 42%; } } - &:hover { - .item { - &.stretchOn { - flex: 0 1 $widthShrunk; - max-width: $widthShrunk; - - &:hover { - flex: 0 0 $widthExpanded; - max-width: $widthExpanded; - } - } - } + .item.stretchOn:hover { + flex: 0 0 $widthExpanded; + max-width: $widthExpanded; } } } -.section+.section { +.section + .section { margin-top: 1em; } From 81b57a57c713d9bc67efd8950ef1c94e0a0238a5 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Tue, 7 Mar 2017 21:17:27 +0000 Subject: [PATCH 82/93] [ci skip] js-precompiled 20170307-211202 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9bd060a1..f4cb28645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#190abe2499d7ab2630ded2a127122b5c5a55f956" +source = "git+https://github.com/ethcore/js-precompiled.git#14be1b9e48ca1347f2c6d3373fec134b22a500b9" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index bf10c8369..e664b356f 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "1.7.1", + "version": "1.7.2", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From f0f2d00924d8a1297a5e245435e409905f93731e Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Wed, 8 Mar 2017 12:00:04 +0100 Subject: [PATCH 83/93] Update the key (#4817) --- js/src/contracts/code/wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/contracts/code/wallet.js b/js/src/contracts/code/wallet.js index 381bd153f..7b172c3f1 100644 --- a/js/src/contracts/code/wallet.js +++ b/js/src/contracts/code/wallet.js @@ -23,7 +23,7 @@ export const wallet = '0x6060604052341561000c57fe5b60405161048538038061048583398 export const walletLibrary = '0x6060604052341561000c57fe5b5b6116d88061001c6000396000f300606060405236156101015763ffffffff60e060020a600035041663173825d981146101575780632f54bf6e146101755780634123cb6b146101a557806352375093146101c75780635c52c2f5146101e9578063659010e7146101fb5780637065cb481461021d578063746c91711461023b578063797af6271461025d5780639da5e0eb14610284578063b20d30a914610299578063b61d27f6146102ae578063b75c7dc6146102ec578063ba51a6df14610301578063c2cf732614610316578063c41a360a14610349578063c57c5f6014610378578063cbf0b0c0146103cf578063e46dcfeb146103ed578063f00d4b5d14610449578063f1736d861461046d575b6101555b60003411156101525760408051600160a060020a033316815234602082015281517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c929181900390910190a15b5b565b005b341561015f57fe5b610155600160a060020a036004351661048f565b005b341561017d57fe5b610191600160a060020a036004351661057d565b604080519115158252519081900360200190f35b34156101ad57fe5b6101b561059e565b60408051918252519081900360200190f35b34156101cf57fe5b6101b56105a4565b60408051918252519081900360200190f35b34156101f157fe5b6101556105aa565b005b341561020357fe5b6101b56105e1565b60408051918252519081900360200190f35b341561022557fe5b610155600160a060020a03600435166105e7565b005b341561024357fe5b6101b56106d7565b60408051918252519081900360200190f35b341561026557fe5b6101916004356106dd565b604080519115158252519081900360200190f35b341561028c57fe5b610155600435610a2f565b005b34156102a157fe5b610155600435610a43565b005b34156102b657fe5b6101b560048035600160a060020a0316906024803591604435918201910135610a7b565b60408051918252519081900360200190f35b34156102f457fe5b610155600435610d5d565b005b341561030957fe5b610155600435610e08565b005b341561031e57fe5b610191600435600160a060020a0360243516610e8a565b604080519115158252519081900360200190f35b341561035157fe5b61035c600435610edf565b60408051600160a060020a039092168252519081900360200190f35b341561038057fe5b6101556004808035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496505093359350610f0092505050565b005b34156103d757fe5b610155600160a060020a0360043516610fd4565b005b34156103f557fe5b6101556004808035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496505084359460200135935061101292505050565b005b341561045157fe5b610155600160a060020a036004358116906024351661102b565b005b341561047557fe5b6101b5611125565b60408051918252519081900360200190f35b60006000366040518083838082843782019150509250505060405180910390206104b88161112b565b1561057657600160a060020a0383166000908152610105602052604090205491508115156104e557610576565b60016001540360005411156104f957610576565b6000600583610100811061050957fe5b0160005b5055600160a060020a03831660009081526101056020526040812055610531611296565b610539611386565b60408051600160a060020a038516815290517f58619076adf5bb0943d100ef88d52d7c3fd691b19d3a9071b555b651fbf418da9181900360200190a15b5b5b505050565b600160a060020a03811660009081526101056020526040812054115b919050565b60015481565b60045481565b6000366040518083838082843782019150509250505060405180910390206105d18161112b565b156105dc5760006003555b5b5b50565b60035481565b60003660405180838380828437820191505092505050604051809103902061060e8161112b565b156106d15761061c8261057d565b15610626576106d1565b61062e611296565b60015460fa901061064157610641611386565b5b60015460fa9010610652576106d1565b60018054810190819055600160a060020a03831690600590610100811061067557fe5b0160005b5055600154600160a060020a03831660008181526101056020908152604091829020939093558051918252517f994a936646fe87ffe4f1e469d3d6aa417d6b855598397f323de5b449f765f0c3929181900390910190a15b5b5b5050565b60005481565b60006000826106eb8161112b565b15610a255760008481526101086020526040902054600160a060020a031615158061072757506000848152610108602052604090206001015415155b80610754575060008481526101086020526040902060029081015461010060018216150260001901160415155b15610a255760008481526101086020526040902054600160a060020a0316151561082c57600084815261010860209081526040918290206001808201546002928301805486516000199482161561010002949094011693909304601f810185900485028301850190955284825261082594909391929183018282801561081b5780601f106107f05761010080835404028352916020019161081b565b820191906000526020600020905b8154815290600101906020018083116107fe57829003601f168201915b50505050506114c2565b91506108e3565b60008481526101086020526040908190208054600180830154935160029384018054600160a060020a0390941695949093919283928592918116156101000260001901160480156108be5780601f10610893576101008083540402835291602001916108be565b820191906000526020600020905b8154815290600101906020018083116108a157829003601f168201915b505091505060006040518083038185876185025a03f19250505015156108e357610000565b5b6000848152610108602090815260409182902060018082015482548551600160a060020a033381811683529682018c90529681018390529086166060820181905295881660a082015260c06080820181815260029586018054958616156101000260001901909516959095049082018190527fe3a3a4111a84df27d76b68dc721e65c7711605ea5eee4afd3a9c58195217365c968b959394909390928a9290919060e0830190859080156109d95780601f106109ae576101008083540402835291602001916109d9565b820191906000526020600020905b8154815290600101906020018083116109bc57829003601f168201915b505097505050505050505060405180910390a16000848152610108602052604081208054600160a060020a03191681556001810182905590610a1e6002830182611557565b5050600192505b5b5b5b5050919050565b6002819055610a3c6114dc565b6004555b50565b600036604051808383808284378201915050925050506040518091039020610a6a8161112b565b156106d15760028290555b5b5b5050565b60006000610a883361057d565b15610d505782158015610a9f5750610a9f856114eb565b5b80610aad57506000546001145b15610bef57600160a060020a0386161515610b0357610afc8585858080601f016020809104026020016040519081016040528093929190818152602001838380828437506114c2945050505050565b9050610b43565b85600160a060020a03168585856040518083838082843782019150509250505060006040518083038185876185025a03f1925050501515610b4357610000565b5b7f9738cd1a8777c86b011f7b01d87d484217dc6ab5154a9d41eda5d14af8caf2923386888787866040518087600160a060020a0316600160a060020a0316815260200186815260200185600160a060020a0316600160a060020a031681526020018060200183600160a060020a0316600160a060020a0316815260200182810382528585828181526020019250808284376040519201829003995090975050505050505050a1610d50565b600036436040518084848082843791909101928352505060408051602092819003830190206000818152610108909352912054909450600160a060020a0316159150508015610c4e575060008281526101086020526040902060010154155b8015610c7b5750600082815261010860205260409020600290810154610100600182161502600019011604155b15610cbf576000828152610108602052604090208054600160a060020a031916600160a060020a03881617815560018101869055610cbd90600201858561159f565b505b610cc8826106dd565b1515610d505760408051838152600160a060020a033381811660208401529282018890528816606082015260a0608082018181529082018690527f1733cbb53659d713b79580f79f3f9ff215f78a7c7aa45890f3b89fc5cddfbf32928592909189918b918a918a9160c082018484808284376040519201829003995090975050505050505050a15b5b5b5b5b50949350505050565b600160a060020a033316600090815261010560205260408120549080821515610d8557610e01565b50506000828152610106602052604081206001810154600284900a929083161115610e015780546001908101825581018054839003905560408051600160a060020a03331681526020810186905281517fc7fb647e59b18047309aa15aad418e5d7ca96d173ad704f1031a2c3d7591734b929181900390910190a15b5b50505050565b600036604051808383808284378201915050925050506040518091039020610e2f8161112b565b156106d157600154821115610e43576106d1565b6000829055610e50611296565b6040805183815290517facbdb084c721332ac59f9b8e392196c9eb0e4932862da8eb9beaf0dad4f550da9181900360200190a15b5b5b5050565b600082815261010660209081526040808320600160a060020a038516845261010590925282205482811515610ec25760009350610ed6565b8160020a9050808360010154166000141593505b50505092915050565b60006005600183016101008110610ef257fe5b0160005b505490505b919050565b815160019081018155600090600160a060020a033316906005905b0160005b505550600160a060020a033316600090815261010560205260408120600190555b8251811015610fc9578281815181101515610f5757fe5b60209081029091010151600160a060020a03166005600283016101008110610f7b57fe5b0160005b50819055508060020161010560008584815181101515610f9b57fe5b90602001906020020151600160a060020a03168152602001908152602001600020819055505b600101610f40565b60008290555b505050565b600036604051808383808284378201915050925050506040518091039020610ffb8161112b565b156106d15781600160a060020a0316ff5b5b5b5050565b61101b81610a2f565b6105768383610f00565b5b505050565b60006000366040518083838082843782019150509250505060405180910390206110548161112b565b15610e01576110628361057d565b1561106c57610e01565b600160a060020a03841660009081526101056020526040902054915081151561109457610e01565b61109c611296565b600160a060020a03831660058361010081106110b457fe5b0160005b5055600160a060020a0380851660008181526101056020908152604080832083905593871680835291849020869055835192835282015281517fb532073b38c83145e3e5135377a08bf9aab55bc0fd7c1179cd4fb995d2a5159c929181900390910190a15b5b5b50505050565b60025481565b600160a060020a0333166000908152610105602052604081205481808215156111535761128c565b600085815261010660205260409020805490925015156111b65760008054835560018084019190915561010780549161118e9190830161161e565b60028301819055610107805487929081106111a557fe5b906000526020600020900160005b50555b8260020a9050808260010154166000141561128c5760408051600160a060020a03331681526020810187905281517fe1c52dc63b719ade82e8bea94cc41a0d5d28e4aaf536adb5e9cccc9ff8c1aeda929181900390910190a181546001901161127957600085815261010660205260409020600201546101078054909190811061123c57fe5b906000526020600020900160005b50600090819055858152610106602052604081208181556001808201839055600290910191909155935061128c565b8154600019018255600182018054821790555b5b5b505050919050565b6101075460005b81811015611374576101086000610107838154811015156112ba57fe5b906000526020600020900160005b50548152602081019190915260400160009081208054600160a060020a031916815560018101829055906112ff6002830182611557565b505061010780548290811061131057fe5b906000526020600020900160005b50541561136b5761010660006101078381548110151561133a57fe5b906000526020600020900160005b505481526020810191909152604001600090812081815560018101829055600201555b5b60010161129d565b6106d16101076000611648565b5b5050565b60015b6001548110156105dc575b600154811080156113b7575060058161010081106113ae57fe5b0160005b505415155b156113c457600101611394565b5b60016001541180156113eb575060015460059061010081106113e357fe5b0160005b5054155b156113ff57600180546000190190556113c4565b600154811080156114255750600154600590610100811061141c57fe5b0160005b505415155b80156114425750600581610100811061143a57fe5b0160005b5054155b156114b957600154600590610100811061145857fe5b0160005b5054600582610100811061146c57fe5b0160005b5055806101056000600583610100811061148657fe5b0160005b505481526020019081526020016000208190555060006005600154610100811015156114b257fe5b0160005b50555b611389565b5b50565b600081516020830184f09050803b15610000575b92915050565b600062015180425b0490505b90565b60006114f63361057d565b15610599576004546115066114dc565b111561151d5760006003556115196114dc565b6004555b600354828101108015906115375750600254826003540111155b1561154c575060038054820190556001610599565b5060005b5b5b919050565b50805460018160011615610100020316600290046000825580601f1061157d57506105dc565b601f0160209004906000526020600020908101906105dc919061166a565b5b50565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106115e05782800160ff1982351617855561160d565b8280016001018555821561160d579182015b8281111561160d5782358255916020019190600101906115f2565b5b5061161a92915061166a565b5090565b8154818355818115116105765760008381526020902061057691810190830161166a565b5b505050565b50805460008255906000526020600020908101906105dc919061166a565b5b50565b6114e891905b8082111561161a5760008155600101611670565b5090565b90565b6114e891905b8082111561161a5760008155600101611670565b5090565b905600a165627a7a723058206560ca68304798da7e3be68397368a30b63db1453ff138ff8f765e80080025af0029'; export const walletLibraryABI = '[{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"removeOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_numOwners","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_lastDay","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"resetSpentToday","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_spentToday","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"addOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_required","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_h","type":"bytes32"}],"name":"confirm","outputs":[{"name":"o_success","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_limit","type":"uint256"}],"name":"initDaylimit","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_newLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"o_hash","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_operation","type":"bytes32"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_newRequired","type":"uint256"}],"name":"changeRequirement","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_operation","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"hasConfirmed","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"ownerIndex","type":"uint256"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"}],"name":"initMultiowned","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"}],"name":"kill","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"},{"name":"_daylimit","type":"uint256"}],"name":"initWallet","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"changeOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"m_dailyLimit","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"payable":true,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Confirmation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Revoke","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"},{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"}],"name":"OwnerRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newRequirement","type":"uint256"}],"name":"RequirementChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"},{"indexed":false,"name":"created","type":"address"}],"name":"SingleTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"},{"indexed":false,"name":"created","type":"address"}],"name":"MultiTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"initiator","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"ConfirmationNeeded","type":"event"}]'; export const walletSourceURL = 'https://github.com/ethcore/parity/blob/63137b15482344ff9df634c086abaabed452eadc/js/src/contracts/snippets/enhanced-wallet.sol'; -export const walletLibraryRegKey = 'walletLibrary'; +export const walletLibraryRegKey = 'walletLibrary.v.2'; // Used if no Wallet Library found in registry... // Compiled from `wallet.sol` using Solidity v0.4.9 - Optimized From f16b53d92af3884e4d2d40c6ad920439eb10bd10 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Wed, 8 Mar 2017 12:00:27 +0100 Subject: [PATCH 84/93] Adjust selection colours/display (#4811) * Adjust selection colours to match with mui * allow -> disable (simplify selections) * Only use top-border * Overlay selection line * Slightly more muted unselected * Restore address icon --- js/src/ui/AccountCard/accountCard.css | 6 ------ js/src/ui/AccountCard/accountCard.js | 14 ++++++------- js/src/ui/Form/AddressSelect/addressSelect.js | 1 - js/src/ui/SelectionList/selectionList.css | 21 ++++++++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/js/src/ui/AccountCard/accountCard.css b/js/src/ui/AccountCard/accountCard.css index 44012cab0..e6cafe656 100644 --- a/js/src/ui/AccountCard/accountCard.css +++ b/js/src/ui/AccountCard/accountCard.css @@ -110,12 +110,6 @@ overflow: hidden; text-overflow: ellipsis; font-size: 0.9em; - - .address { - &:hover { - cursor: text; - } - } } .accountName { diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index d7b455132..b5746bf82 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -28,15 +28,15 @@ import styles from './accountCard.css'; export default class AccountCard extends Component { static propTypes = { account: PropTypes.object.isRequired, - allowAddressClick: PropTypes.bool, balance: PropTypes.object, className: PropTypes.string, + disableAddressClick: PropTypes.bool, onClick: PropTypes.func, onFocus: PropTypes.func }; static defaultProps = { - allowAddressClick: false + disableAddressClick: false }; state = { @@ -138,14 +138,14 @@ export default class AccountCard extends Component { } handleAddressClick = (event) => { - const { allowAddressClick } = this.props; + const { disableAddressClick } = this.props; - // Don't stop the event if address click is allowed - if (allowAddressClick) { - return this.onClick(event); + // Stop the event if address click is disallowed + if (disableAddressClick) { + return this.preventEvent(event); } - return this.preventEvent(event); + return this.onClick(event); } handleKeyDown = (event) => { diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 785a8fa65..511ab930f 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -348,7 +348,6 @@ class AddressSelect extends Component { return ( <AccountCard account={ account } - allowAddressClick balance={ balance } className={ styles.account } key={ `account_${index}` } diff --git a/js/src/ui/SelectionList/selectionList.css b/js/src/ui/SelectionList/selectionList.css index 6a1a37eaf..b6e6b05f9 100644 --- a/js/src/ui/SelectionList/selectionList.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -16,7 +16,6 @@ */ .item { - border: 2px solid transparent; cursor: pointer; display: flex; flex: 1; @@ -25,7 +24,6 @@ width: 100%; &:hover { - border-color: transparent; filter: none; opacity: 1; } @@ -35,7 +33,7 @@ width: 100%; &:hover { - background-color: rgba(255, 255, 255, 0.5); + background-color: rgba(255, 255, 255, 0.15); } } @@ -68,15 +66,24 @@ } .selected { - border-color: rgba(255, 255, 255, 0.25); filter: none; - &.default { - border-color: rgba(255, 255, 255, 0.75); + &::after { + background: rgb(0, 151, 167); + content: ''; + height: 4px; + left: 0; + position: absolute; + right: 0; + top: 0; + } + + &.default::after { + background: rgb(167, 151, 0); } } .unselected { - filter: grayscale(10%); + filter: grayscale(50%); opacity: 0.75; } From 94a39619b55d9fc04281b5f3dc136480b0393f95 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Wed, 8 Mar 2017 12:26:03 +0100 Subject: [PATCH 85/93] Fix default values for contract queries (#4819) --- js/src/views/Contract/Queries/inputQuery.js | 35 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/js/src/views/Contract/Queries/inputQuery.js b/js/src/views/Contract/Queries/inputQuery.js index a93fe74d0..8d4bddf02 100644 --- a/js/src/views/Contract/Queries/inputQuery.js +++ b/js/src/views/Contract/Queries/inputQuery.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { isEqual } from 'lodash'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import LinearProgress from 'material-ui/LinearProgress'; @@ -24,6 +25,7 @@ import { bindActionCreators } from 'redux'; import { newError } from '~/redux/actions'; import { Button, TypedInput } from '~/ui'; import { arrayOrObjectProptype } from '~/util/proptypes'; +import { parseAbiType } from '~/util/abi'; import styles from './queries.css'; @@ -44,11 +46,35 @@ class InputQuery extends Component { }; state = { + inputs: [], isValid: true, results: [], values: {} }; + componentWillMount () { + this.parseInputs(); + } + + componentWillReceiveProps (nextProps) { + const prevInputTypes = this.props.inputs.map((input) => input.type); + const nextInputTypes = nextProps.inputs.map((input) => input.type); + + if (!isEqual(prevInputTypes, nextInputTypes)) { + this.parseInputs(nextProps); + } + } + + parseInputs (props = this.props) { + const inputs = props.inputs.map((input) => ({ ...input, parsed: parseAbiType(input.type) })); + const values = inputs.reduce((values, input, index) => { + values[index] = input.parsed.default; + return values; + }, {}); + + this.setState({ inputs, values }); + } + render () { const { name, className } = this.props; @@ -64,10 +90,9 @@ class InputQuery extends Component { } renderContent () { - const { inputs } = this.props; + const { inputs } = this.state; const { isValid } = this.state; - const inputsFields = inputs .map((input, index) => this.renderInput(input, index)); @@ -190,15 +215,15 @@ class InputQuery extends Component { } onClick = () => { - const { values } = this.state; - const { inputs, contract, name, outputs, signature } = this.props; + const { inputs, values } = this.state; + const { contract, name, outputs, signature } = this.props; this.setState({ isLoading: true, results: [] }); - const inputValues = inputs.map((input, index) => values[index] || ''); + const inputValues = inputs.map((input, index) => values[index]); contract .instance[signature] From 02c51c83cd5d6b1802cd1b276f9a184ef8ca41be Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Wed, 8 Mar 2017 13:21:39 +0100 Subject: [PATCH 86/93] Better logic for contract deployments (#4821) --- .../ui/MethodDecoding/methodDecodingStore.js | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js index da23f916c..05a8f8546 100644 --- a/js/src/ui/MethodDecoding/methodDecodingStore.js +++ b/js/src/ui/MethodDecoding/methodDecodingStore.js @@ -148,7 +148,19 @@ export default class MethodDecodingStore { // Contract deployment if (!signature || signature === CONTRACT_CREATE || transaction.creates) { - return this.decodeContractCreation(result, contractAddress || transaction.creates); + const address = contractAddress || transaction.creates; + + return this.isContractCreation(input, address) + .then((isContractCreation) => { + if (!isContractCreation) { + result.contract = false; + result.deploy = false; + + return result; + } + + return this.decodeContractCreation(result, address); + }); } return this @@ -204,7 +216,7 @@ export default class MethodDecodingStore { const { input } = data; const abi = this._contractsAbi[contractAddress]; - if (!input || !abi || !abi.constructors || abi.constructors.length === 0) { + if (!abi || !abi.constructors || abi.constructors.length === 0) { return Promise.resolve(result); } @@ -306,6 +318,30 @@ export default class MethodDecodingStore { return Promise.resolve(this._isContract[contractAddress]); } + /** + * Check if the input resulted in a contract creation + * by checking that the contract address code contains + * a part of the input, or vice-versa + */ + isContractCreation (input, contractAddress) { + return this.api.eth + .getCode(contractAddress) + .then((code) => { + if (/^(0x)?0*$/.test(code)) { + return false; + } + + const strippedCode = code.replace(/^0x/, ''); + const strippedInput = input.replace(/^0x/, ''); + + return strippedInput.indexOf(strippedInput) >= 0 || strippedCode.indexOf(strippedInput) >= 0; + }) + .catch((error) => { + console.error(error); + return false; + }); + } + getCode (contractAddress) { // If zero address, resolve to '0x' if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) { From 5bbcf0482b6c8a4c8d4631db31c73920dfd08e07 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 8 Mar 2017 12:38:37 +0000 Subject: [PATCH 87/93] [ci skip] js-precompiled 20170308-122756 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4cb28645..6924cfe00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#14be1b9e48ca1347f2c6d3373fec134b22a500b9" +source = "git+https://github.com/ethcore/js-precompiled.git#9eef2b78d363560fe942062caaaa7f6b1d64dd17" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index e664b356f..85d906b88 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "1.7.2", + "version": "1.7.3", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 8a3b5c633290ff755bcfffa91cb2213deae7cb0a Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Wed, 8 Mar 2017 14:39:44 +0100 Subject: [PATCH 88/93] Remote transaction execution (#4684) * return errors on database corruption * fix tests, json tests * fix remainder of build * buffer flow -> request credits * proving state backend * generate transaction proofs from provider * network messages for transaction proof * transaction proof test * test for transaction proof message * fix call bug * request transaction proofs from on_demand * most of proved_execution rpc * proved execution future --- ethcore/light/src/client/mod.rs | 30 ++- ethcore/light/src/net/error.rs | 8 +- ethcore/light/src/net/mod.rs | 194 ++++++++++++++---- .../{buffer_flow.rs => request_credits.rs} | 87 ++++---- ethcore/light/src/net/request_set.rs | 1 + ethcore/light/src/net/status.rs | 8 +- ethcore/light/src/net/tests/mod.rs | 86 ++++++-- ethcore/light/src/on_demand/mod.rs | 81 +++++++- ethcore/light/src/on_demand/request.rs | 35 +++- ethcore/light/src/provider.rs | 30 ++- ethcore/light/src/types/les_request.rs | 33 ++- ethcore/src/client/client.rs | 94 ++++----- ethcore/src/client/test_client.rs | 4 + ethcore/src/client/traits.rs | 4 + ethcore/src/env_info.rs | 4 +- ethcore/src/lib.rs | 3 +- ethcore/src/state/backend.rs | 156 ++++++++++++-- ethcore/src/state/mod.rs | 75 ++++++- ethcore/src/tests/client.rs | 43 +++- rpc/src/v1/helpers/dispatch.rs | 99 +++++---- rpc/src/v1/impls/eth.rs | 28 ++- rpc/src/v1/impls/light/eth.rs | 123 +++++++++-- rpc/src/v1/traits/eth.rs | 8 +- sync/src/light_sync/tests/test_net.rs | 2 +- util/src/hashdb.rs | 10 + 25 files changed, 993 insertions(+), 253 deletions(-) rename ethcore/light/src/net/{buffer_flow.rs => request_credits.rs} (77%) diff --git a/ethcore/light/src/client/mod.rs b/ethcore/light/src/client/mod.rs index 5701fc606..2872e0eec 100644 --- a/ethcore/light/src/client/mod.rs +++ b/ethcore/light/src/client/mod.rs @@ -31,7 +31,7 @@ use ethcore::service::ClientIoMessage; use ethcore::encoded; use io::IoChannel; -use util::{Bytes, H256, Mutex, RwLock}; +use util::{Bytes, DBValue, H256, Mutex, RwLock}; use self::header_chain::{AncestryIter, HeaderChain}; @@ -230,22 +230,32 @@ impl Client { } /// Get a handle to the verification engine. - pub fn engine(&self) -> &Engine { - &*self.engine + pub fn engine(&self) -> &Arc<Engine> { + &self.engine } - fn latest_env_info(&self) -> EnvInfo { - let header = self.best_block_header(); + /// Get the latest environment info. + pub fn latest_env_info(&self) -> EnvInfo { + self.env_info(BlockId::Latest) + .expect("Best block header and recent hashes always stored; qed") + } - EnvInfo { + /// Get environment info for a given block. + pub fn env_info(&self, id: BlockId) -> Option<EnvInfo> { + let header = match self.block_header(id) { + Some(hdr) => hdr, + None => return None, + }; + + Some(EnvInfo { number: header.number(), author: header.author(), timestamp: header.timestamp(), difficulty: header.difficulty(), - last_hashes: self.build_last_hashes(header.hash()), + last_hashes: self.build_last_hashes(header.parent_hash()), gas_used: Default::default(), gas_limit: header.gas_limit(), - } + }) } fn build_last_hashes(&self, mut parent_hash: H256) -> Arc<Vec<H256>> { @@ -344,6 +354,10 @@ impl ::provider::Provider for Client { None } + fn transaction_proof(&self, _req: ::request::TransactionProof) -> Option<Vec<DBValue>> { + None + } + fn ready_transactions(&self) -> Vec<::ethcore::transaction::PendingTransaction> { Vec::new() } diff --git a/ethcore/light/src/net/error.rs b/ethcore/light/src/net/error.rs index 627a7ef0f..dda78e0b6 100644 --- a/ethcore/light/src/net/error.rs +++ b/ethcore/light/src/net/error.rs @@ -44,8 +44,8 @@ pub enum Error { Rlp(DecoderError), /// A network error. Network(NetworkError), - /// Out of buffer. - BufferEmpty, + /// Out of credits. + NoCredits, /// Unrecognized packet code. UnrecognizedPacket(u8), /// Unexpected handshake. @@ -72,7 +72,7 @@ impl Error { match *self { Error::Rlp(_) => Punishment::Disable, Error::Network(_) => Punishment::None, - Error::BufferEmpty => Punishment::Disable, + Error::NoCredits => Punishment::Disable, Error::UnrecognizedPacket(_) => Punishment::Disconnect, Error::UnexpectedHandshake => Punishment::Disconnect, Error::WrongNetwork => Punishment::Disable, @@ -103,7 +103,7 @@ impl fmt::Display for Error { match *self { Error::Rlp(ref err) => err.fmt(f), Error::Network(ref err) => err.fmt(f), - Error::BufferEmpty => write!(f, "Out of buffer"), + Error::NoCredits => write!(f, "Out of request credits"), Error::UnrecognizedPacket(code) => write!(f, "Unrecognized packet: 0x{:x}", code), Error::UnexpectedHandshake => write!(f, "Unexpected handshake"), Error::WrongNetwork => write!(f, "Wrong network"), diff --git a/ethcore/light/src/net/mod.rs b/ethcore/light/src/net/mod.rs index eb5677cfa..181f95e95 100644 --- a/ethcore/light/src/net/mod.rs +++ b/ethcore/light/src/net/mod.rs @@ -19,14 +19,14 @@ //! This uses a "Provider" to answer requests. //! See https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES) -use ethcore::transaction::UnverifiedTransaction; +use ethcore::transaction::{Action, UnverifiedTransaction}; use ethcore::receipt::Receipt; use io::TimerToken; use network::{NetworkProtocolHandler, NetworkContext, PeerId}; use rlp::{RlpStream, Stream, UntrustedRlp, View}; use util::hash::H256; -use util::{Bytes, Mutex, RwLock, U256}; +use util::{Bytes, DBValue, Mutex, RwLock, U256}; use time::{Duration, SteadyTime}; use std::collections::HashMap; @@ -37,7 +37,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use provider::Provider; use request::{self, HashOrNumber, Request}; -use self::buffer_flow::{Buffer, FlowParams}; +use self::request_credits::{Credits, FlowParams}; use self::context::{Ctx, TickCtx}; use self::error::Punishment; use self::request_set::RequestSet; @@ -51,7 +51,7 @@ mod request_set; #[cfg(test)] mod tests; -pub mod buffer_flow; +pub mod request_credits; pub use self::error::Error; pub use self::context::{BasicContext, EventContext, IoContext}; @@ -73,7 +73,7 @@ pub const PROTOCOL_VERSIONS: &'static [u8] = &[1]; pub const MAX_PROTOCOL_VERSION: u8 = 1; /// Packet count for LES. -pub const PACKET_COUNT: u8 = 15; +pub const PACKET_COUNT: u8 = 17; // packet ID definitions. mod packet { @@ -109,6 +109,10 @@ mod packet { // request and response for header proofs in a CHT. pub const GET_HEADER_PROOFS: u8 = 0x0d; pub const HEADER_PROOFS: u8 = 0x0e; + + // request and response for transaction proof. + pub const GET_TRANSACTION_PROOF: u8 = 0x0f; + pub const TRANSACTION_PROOF: u8 = 0x10; } // timeouts for different kinds of requests. all values are in milliseconds. @@ -121,6 +125,7 @@ mod timeout { pub const PROOFS: i64 = 4000; pub const CONTRACT_CODES: i64 = 5000; pub const HEADER_PROOFS: i64 = 3500; + pub const TRANSACTION_PROOF: i64 = 5000; } /// A request id. @@ -143,10 +148,10 @@ struct PendingPeer { /// Relevant data to each peer. Not accessible publicly, only `pub` due to /// limitations of the privacy system. pub struct Peer { - local_buffer: Buffer, // their buffer relative to us + local_credits: Credits, // their credits relative to us status: Status, capabilities: Capabilities, - remote_flow: Option<(Buffer, FlowParams)>, + remote_flow: Option<(Credits, FlowParams)>, sent_head: H256, // last chain head we've given them. last_update: SteadyTime, pending_requests: RequestSet, @@ -155,21 +160,21 @@ pub struct Peer { impl Peer { // check the maximum cost of a request, returning an error if there's - // not enough buffer left. + // not enough credits left. // returns the calculated maximum cost. fn deduct_max(&mut self, flow_params: &FlowParams, kind: request::Kind, max: usize) -> Result<U256, Error> { - flow_params.recharge(&mut self.local_buffer); + flow_params.recharge(&mut self.local_credits); let max_cost = flow_params.compute_cost(kind, max); - self.local_buffer.deduct_cost(max_cost)?; + self.local_credits.deduct_cost(max_cost)?; Ok(max_cost) } - // refund buffer for a request. returns new buffer amount. + // refund credits for a request. returns new amount of credits. fn refund(&mut self, flow_params: &FlowParams, amount: U256) -> U256 { - flow_params.refund(&mut self.local_buffer, amount); + flow_params.refund(&mut self.local_credits, amount); - self.local_buffer.current() + self.local_credits.current() } } @@ -206,6 +211,8 @@ pub trait Handler: Send + Sync { /// Called when a peer responds with header proofs. Each proof should be a block header coupled /// with a series of trie nodes is ascending order by distance from the root. fn on_header_proofs(&self, _ctx: &EventContext, _req_id: ReqId, _proofs: &[(Bytes, Vec<Bytes>)]) { } + /// Called when a peer responds with a transaction proof. Each proof is a vector of state items. + fn on_transaction_proof(&self, _ctx: &EventContext, _req_id: ReqId, _state_items: &[DBValue]) { } /// Called to "tick" the handler periodically. fn tick(&self, _ctx: &BasicContext) { } /// Called on abort. This signals to handlers that they should clean up @@ -218,7 +225,7 @@ pub trait Handler: Send + Sync { pub struct Params { /// Network id. pub network_id: u64, - /// Buffer flow parameters. + /// Request credits parameters. pub flow_params: FlowParams, /// Initial capabilities. pub capabilities: Capabilities, @@ -334,14 +341,14 @@ impl LightProtocol { /// Check the maximum amount of requests of a specific type /// which a peer would be able to serve. Returns zero if the - /// peer is unknown or has no buffer flow parameters. + /// peer is unknown or has no credit parameters. fn max_requests(&self, peer: PeerId, kind: request::Kind) -> usize { self.peers.read().get(&peer).and_then(|peer| { let mut peer = peer.lock(); match peer.remote_flow { - Some((ref mut buf, ref flow)) => { - flow.recharge(buf); - Some(flow.max_amount(&*buf, kind)) + Some((ref mut c, ref flow)) => { + flow.recharge(c); + Some(flow.max_amount(&*c, kind)) } None => None, } @@ -351,7 +358,7 @@ impl LightProtocol { /// Make a request to a peer. /// /// Fails on: nonexistent peer, network error, peer not server, - /// insufficient buffer. Does not check capabilities before sending. + /// insufficient credits. Does not check capabilities before sending. /// On success, returns a request id which can later be coordinated /// with an event. pub fn request_from(&self, io: &IoContext, peer_id: &PeerId, request: Request) -> Result<ReqId, Error> { @@ -360,10 +367,10 @@ impl LightProtocol { let mut peer = peer.lock(); match peer.remote_flow { - Some((ref mut buf, ref flow)) => { - flow.recharge(buf); + Some((ref mut c, ref flow)) => { + flow.recharge(c); let max = flow.compute_cost(request.kind(), request.amount()); - buf.deduct_cost(max)?; + c.deduct_cost(max)?; } None => return Err(Error::NotServer), } @@ -380,6 +387,7 @@ impl LightProtocol { request::Kind::StateProofs => packet::GET_PROOFS, request::Kind::Codes => packet::GET_CONTRACT_CODES, request::Kind::HeaderProofs => packet::GET_HEADER_PROOFS, + request::Kind::TransactionProof => packet::GET_TRANSACTION_PROOF, }; io.send(*peer_id, packet_id, packet_data); @@ -464,7 +472,7 @@ impl LightProtocol { // - check whether request kinds match fn pre_verify_response(&self, peer: &PeerId, kind: request::Kind, raw: &UntrustedRlp) -> Result<IdGuard, Error> { let req_id = ReqId(raw.val_at(0)?); - let cur_buffer: U256 = raw.val_at(1)?; + let cur_credits: U256 = raw.val_at(1)?; trace!(target: "les", "pre-verifying response from peer {}, kind={:?}", peer, kind); @@ -480,9 +488,9 @@ impl LightProtocol { (Some(request), Some(flow_info)) => { had_req = true; - let &mut (ref mut buf, ref mut flow) = flow_info; - let actual_buffer = ::std::cmp::min(cur_buffer, *flow.limit()); - buf.update_to(actual_buffer); + let &mut (ref mut c, ref mut flow) = flow_info; + let actual_credits = ::std::cmp::min(cur_credits, *flow.limit()); + c.update_to(actual_credits); if request.kind() != kind { Some(Error::UnsolicitedResponse) @@ -539,6 +547,9 @@ impl LightProtocol { packet::GET_HEADER_PROOFS => self.get_header_proofs(peer, io, rlp), packet::HEADER_PROOFS => self.header_proofs(peer, io, rlp), + packet::GET_TRANSACTION_PROOF => self.get_transaction_proof(peer, io, rlp), + packet::TRANSACTION_PROOF => self.transaction_proof(peer, io, rlp), + packet::SEND_TRANSACTIONS => self.relay_transactions(peer, io, rlp), other => { @@ -685,10 +696,10 @@ impl LightProtocol { return Err(Error::BadProtocolVersion); } - let remote_flow = flow_params.map(|params| (params.create_buffer(), params)); + let remote_flow = flow_params.map(|params| (params.create_credits(), params)); self.peers.write().insert(*peer, Mutex::new(Peer { - local_buffer: self.flow_params.create_buffer(), + local_credits: self.flow_params.create_credits(), status: status.clone(), capabilities: capabilities.clone(), remote_flow: remote_flow, @@ -793,10 +804,10 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::Headers, response.len()); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::BLOCK_HEADERS, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for header in response { stream.append_raw(&header.into_inner(), 1); @@ -855,11 +866,11 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::Bodies, response_len); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::BLOCK_BODIES, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for body in response { match body { @@ -921,11 +932,11 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::Receipts, response_len); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::RECEIPTS, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for receipts in response { stream.append_raw(&receipts, 1); @@ -995,11 +1006,11 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::StateProofs, response_len); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::PROOFS, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for proof in response { stream.append_raw(&proof, 1); @@ -1067,11 +1078,11 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::Codes, response_len); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::CONTRACT_CODES, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for code in response { stream.append(&code); @@ -1140,11 +1151,11 @@ impl LightProtocol { let actual_cost = self.flow_params.compute_cost(request::Kind::HeaderProofs, response_len); assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost."); - let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); + let cur_credits = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::HEADER_PROOFS, { let mut stream = RlpStream::new_list(3); - stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); for proof in response { stream.append_raw(&proof, 1); @@ -1182,6 +1193,90 @@ impl LightProtocol { Ok(()) } + // Receive a request for proof-of-execution. + fn get_transaction_proof(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { + // refuse to execute more than this amount of gas at once. + // this is appx. the point at which the proof of execution would no longer fit in + // a single Devp2p packet. + const MAX_GAS: usize = 50_000_000; + use util::Uint; + + let peers = self.peers.read(); + let peer = match peers.get(peer) { + Some(peer) => peer, + None => { + debug!(target: "les", "Ignoring request from unknown peer"); + return Ok(()) + } + }; + let mut peer = peer.lock(); + + let req_id: u64 = raw.val_at(0)?; + + let req = { + let req_rlp = raw.at(1)?; + request::TransactionProof { + at: req_rlp.val_at(0)?, + from: req_rlp.val_at(1)?, + action: if req_rlp.at(2)?.is_empty() { + Action::Create + } else { + Action::Call(req_rlp.val_at(2)?) + }, + gas: ::std::cmp::min(req_rlp.val_at(3)?, MAX_GAS.into()), + gas_price: req_rlp.val_at(4)?, + value: req_rlp.val_at(5)?, + data: req_rlp.val_at(6)?, + } + }; + + // always charge the peer for all the gas. + peer.deduct_max(&self.flow_params, request::Kind::TransactionProof, req.gas.low_u64() as usize)?; + + let response = match self.provider.transaction_proof(req) { + Some(res) => res, + None => vec![], + }; + + let cur_credits = peer.local_credits.current(); + + io.respond(packet::TRANSACTION_PROOF, { + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_credits).begin_list(response.len()); + + for state_item in response { + stream.append(&&state_item[..]); + } + + stream.out() + }); + + Ok(()) + } + + // Receive a response for proof-of-execution. + fn transaction_proof(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { + let id_guard = self.pre_verify_response(peer, request::Kind::HeaderProofs, &raw)?; + let raw_proof: Vec<DBValue> = raw.at(2)?.iter() + .map(|rlp| { + let mut db_val = DBValue::new(); + db_val.append_slice(rlp.data()?); + Ok(db_val) + }) + .collect::<Result<Vec<_>, ::rlp::DecoderError>>()?; + + let req_id = id_guard.defuse(); + for handler in &self.handlers { + handler.on_transaction_proof(&Ctx { + peer: *peer, + io: io, + proto: self, + }, req_id, &raw_proof); + } + + Ok(()) + } + // Receive a set of transactions to relay. fn relay_transactions(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> { const MAX_TRANSACTIONS: usize = 256; @@ -1330,6 +1425,25 @@ fn encode_request(req: &Request, req_id: usize) -> Vec<u8> { .append(&proof_req.from_level); } + stream.out() + } + Request::TransactionProof(ref request) => { + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(7) + .append(&request.at) + .append(&request.from); + + match request.action { + Action::Create => stream.append_empty_data(), + Action::Call(ref to) => stream.append(to), + }; + + stream + .append(&request.gas) + .append(&request.gas_price) + .append(&request.value) + .append(&request.data); + stream.out() } } diff --git a/ethcore/light/src/net/buffer_flow.rs b/ethcore/light/src/net/request_credits.rs similarity index 77% rename from ethcore/light/src/net/buffer_flow.rs rename to ethcore/light/src/net/request_credits.rs index cce54da59..97aa9b431 100644 --- a/ethcore/light/src/net/buffer_flow.rs +++ b/ethcore/light/src/net/request_credits.rs @@ -14,14 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -//! LES buffer flow management. +//! Request credit management. //! -//! Every request in the LES protocol leads to a reduction -//! of the requester's buffer value as a rate-limiting mechanism. -//! This buffer value will recharge at a set rate. +//! Every request in the light protocol leads to a reduction +//! of the requester's amount of credits as a rate-limiting mechanism. +//! The amount of credits will recharge at a set rate. //! -//! This module provides an interface for configuration of buffer -//! flow costs and recharge rates. +//! This module provides an interface for configuration of +//! costs and recharge rates of request credits. //! //! Current default costs are picked completely arbitrarily, not based //! on any empirical timings or mathematical models. @@ -38,19 +38,19 @@ use time::{Duration, SteadyTime}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cost(pub U256, pub U256); -/// Buffer value. +/// Credits value. /// /// Produced and recharged using `FlowParams`. /// Definitive updates can be made as well -- these will reset the recharge /// point to the time of the update. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Buffer { +pub struct Credits { estimate: U256, recharge_point: SteadyTime, } -impl Buffer { - /// Get the current buffer value. +impl Credits { + /// Get the current amount of credits.. pub fn current(&self) -> U256 { self.estimate.clone() } /// Make a definitive update. @@ -61,7 +61,7 @@ impl Buffer { self.recharge_point = SteadyTime::now(); } - /// Attempt to apply the given cost to the buffer. + /// Attempt to apply the given cost to the amount of credits. /// /// If successful, the cost will be deducted successfully. /// @@ -69,7 +69,7 @@ impl Buffer { /// error will be produced. pub fn deduct_cost(&mut self, cost: U256) -> Result<(), Error> { match cost > self.estimate { - true => Err(Error::BufferEmpty), + true => Err(Error::NoCredits), false => { self.estimate = self.estimate - cost; Ok(()) @@ -81,12 +81,13 @@ impl Buffer { /// A cost table, mapping requests to base and per-request costs. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CostTable { - headers: Cost, + headers: Cost, // cost per header bodies: Cost, receipts: Cost, state_proofs: Cost, contract_codes: Cost, header_proofs: Cost, + transaction_proof: Cost, // cost per gas. } impl Default for CostTable { @@ -99,6 +100,7 @@ impl Default for CostTable { state_proofs: Cost(250000.into(), 25000.into()), contract_codes: Cost(200000.into(), 20000.into()), header_proofs: Cost(150000.into(), 15000.into()), + transaction_proof: Cost(100000.into(), 2.into()), } } } @@ -112,7 +114,7 @@ impl RlpEncodable for CostTable { .append(&cost.1); } - s.begin_list(6); + s.begin_list(7); append_cost(s, packet::GET_BLOCK_HEADERS, &self.headers); append_cost(s, packet::GET_BLOCK_BODIES, &self.bodies); @@ -120,6 +122,7 @@ impl RlpEncodable for CostTable { append_cost(s, packet::GET_PROOFS, &self.state_proofs); append_cost(s, packet::GET_CONTRACT_CODES, &self.contract_codes); append_cost(s, packet::GET_HEADER_PROOFS, &self.header_proofs); + append_cost(s, packet::GET_TRANSACTION_PROOF, &self.transaction_proof); } } @@ -133,6 +136,7 @@ impl RlpDecodable for CostTable { let mut state_proofs = None; let mut contract_codes = None; let mut header_proofs = None; + let mut transaction_proof = None; for row in rlp.iter() { let msg_id: u8 = row.val_at(0)?; @@ -150,6 +154,7 @@ impl RlpDecodable for CostTable { packet::GET_PROOFS => state_proofs = Some(cost), packet::GET_CONTRACT_CODES => contract_codes = Some(cost), packet::GET_HEADER_PROOFS => header_proofs = Some(cost), + packet::GET_TRANSACTION_PROOF => transaction_proof = Some(cost), _ => return Err(DecoderError::Custom("Unrecognized message in cost table")), } } @@ -161,11 +166,12 @@ impl RlpDecodable for CostTable { state_proofs: state_proofs.ok_or(DecoderError::Custom("No proofs cost specified"))?, contract_codes: contract_codes.ok_or(DecoderError::Custom("No contract codes specified"))?, header_proofs: header_proofs.ok_or(DecoderError::Custom("No header proofs cost specified"))?, + transaction_proof: transaction_proof.ok_or(DecoderError::Custom("No transaction proof gas cost specified"))?, }) } } -/// A buffer-flow manager handles costs, recharge, limits +/// Handles costs, recharge, limits of request credits. #[derive(Debug, Clone, PartialEq)] pub struct FlowParams { costs: CostTable, @@ -175,7 +181,7 @@ pub struct FlowParams { impl FlowParams { /// Create new flow parameters from a request cost table, - /// buffer limit, and (minimum) rate of recharge. + /// credit limit, and (minimum) rate of recharge. pub fn new(limit: U256, costs: CostTable, recharge: U256) -> Self { FlowParams { costs: costs, @@ -197,11 +203,12 @@ impl FlowParams { state_proofs: free_cost.clone(), contract_codes: free_cost.clone(), header_proofs: free_cost.clone(), + transaction_proof: free_cost, } } } - /// Get a reference to the buffer limit. + /// Get a reference to the credit limit. pub fn limit(&self) -> &U256 { &self.limit } /// Get a reference to the cost table. @@ -220,6 +227,7 @@ impl FlowParams { request::Kind::StateProofs => &self.costs.state_proofs, request::Kind::Codes => &self.costs.contract_codes, request::Kind::HeaderProofs => &self.costs.header_proofs, + request::Kind::TransactionProof => &self.costs.transaction_proof, }; let amount: U256 = amount.into(); @@ -227,10 +235,10 @@ impl FlowParams { } /// Compute the maximum number of costs of a specific kind which can be made - /// with the given buffer. + /// with the given amount of credits /// Saturates at `usize::max()`. This is not a problem in practice because /// this amount of requests is already prohibitively large. - pub fn max_amount(&self, buffer: &Buffer, kind: request::Kind) -> usize { + pub fn max_amount(&self, credits: &Credits, kind: request::Kind) -> usize { use util::Uint; use std::usize; @@ -241,9 +249,10 @@ impl FlowParams { request::Kind::StateProofs => &self.costs.state_proofs, request::Kind::Codes => &self.costs.contract_codes, request::Kind::HeaderProofs => &self.costs.header_proofs, + request::Kind::TransactionProof => &self.costs.transaction_proof, }; - let start = buffer.current(); + let start = credits.current(); if start <= cost.0 { return 0; @@ -259,36 +268,36 @@ impl FlowParams { } } - /// Create initial buffer parameter. - pub fn create_buffer(&self) -> Buffer { - Buffer { + /// Create initial credits.. + pub fn create_credits(&self) -> Credits { + Credits { estimate: self.limit, recharge_point: SteadyTime::now(), } } - /// Recharge the buffer based on time passed since last + /// Recharge the given credits based on time passed since last /// update. - pub fn recharge(&self, buf: &mut Buffer) { + pub fn recharge(&self, credits: &mut Credits) { let now = SteadyTime::now(); // recompute and update only in terms of full seconds elapsed // in order to keep the estimate as an underestimate. - let elapsed = (now - buf.recharge_point).num_seconds(); - buf.recharge_point = buf.recharge_point + Duration::seconds(elapsed); + let elapsed = (now - credits.recharge_point).num_seconds(); + credits.recharge_point = credits.recharge_point + Duration::seconds(elapsed); let elapsed: U256 = elapsed.into(); - buf.estimate = ::std::cmp::min(self.limit, buf.estimate + (elapsed * self.recharge)); + credits.estimate = ::std::cmp::min(self.limit, credits.estimate + (elapsed * self.recharge)); } - /// Refund some buffer which was previously deducted. + /// Refund some credits which were previously deducted. /// Does not update the recharge timestamp. - pub fn refund(&self, buf: &mut Buffer, refund_amount: U256) { - buf.estimate = buf.estimate + refund_amount; + pub fn refund(&self, credits: &mut Credits, refund_amount: U256) { + credits.estimate = credits.estimate + refund_amount; - if buf.estimate > self.limit { - buf.estimate = self.limit + if credits.estimate > self.limit { + credits.estimate = self.limit } } } @@ -318,20 +327,20 @@ mod tests { } #[test] - fn buffer_mechanism() { + fn credits_mechanism() { use std::thread; use std::time::Duration; let flow_params = FlowParams::new(100.into(), Default::default(), 20.into()); - let mut buffer = flow_params.create_buffer(); + let mut credits = flow_params.create_credits(); - assert!(buffer.deduct_cost(101.into()).is_err()); - assert!(buffer.deduct_cost(10.into()).is_ok()); + assert!(credits.deduct_cost(101.into()).is_err()); + assert!(credits.deduct_cost(10.into()).is_ok()); thread::sleep(Duration::from_secs(1)); - flow_params.recharge(&mut buffer); + flow_params.recharge(&mut credits); - assert_eq!(buffer.estimate, 100.into()); + assert_eq!(credits.estimate, 100.into()); } } diff --git a/ethcore/light/src/net/request_set.rs b/ethcore/light/src/net/request_set.rs index 9a26b24b1..e6d4068da 100644 --- a/ethcore/light/src/net/request_set.rs +++ b/ethcore/light/src/net/request_set.rs @@ -101,6 +101,7 @@ impl RequestSet { request::Kind::StateProofs => timeout::PROOFS, request::Kind::Codes => timeout::CONTRACT_CODES, request::Kind::HeaderProofs => timeout::HEADER_PROOFS, + request::Kind::TransactionProof => timeout::TRANSACTION_PROOF, }; base + Duration::milliseconds(kind_timeout) <= now diff --git a/ethcore/light/src/net/status.rs b/ethcore/light/src/net/status.rs index 655dc404f..3e32f6609 100644 --- a/ethcore/light/src/net/status.rs +++ b/ethcore/light/src/net/status.rs @@ -19,7 +19,7 @@ use rlp::{DecoderError, RlpDecodable, RlpEncodable, RlpStream, Stream, UntrustedRlp, View}; use util::{H256, U256}; -use super::buffer_flow::FlowParams; +use super::request_credits::FlowParams; // recognized handshake/announcement keys. // unknown keys are to be skipped, known keys have a defined order. @@ -207,7 +207,7 @@ impl Capabilities { /// Attempt to parse a handshake message into its three parts: /// - chain status /// - serving capabilities -/// - buffer flow parameters +/// - request credit parameters pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, Option<FlowParams>), DecoderError> { let mut parser = Parser { pos: 0, @@ -300,7 +300,7 @@ pub struct Announcement { pub serve_chain_since: Option<u64>, /// optional new transaction-relay capability. false means "no change" pub tx_relay: bool, - // TODO: changes in buffer flow? + // TODO: changes in request credits. } /// Parse an announcement. @@ -372,7 +372,7 @@ pub fn write_announcement(announcement: &Announcement) -> Vec<u8> { #[cfg(test)] mod tests { use super::*; - use super::super::buffer_flow::FlowParams; + use super::super::request_credits::FlowParams; use util::{U256, H256, FixedHash}; use rlp::{RlpStream, Stream ,UntrustedRlp, View}; diff --git a/ethcore/light/src/net/tests/mod.rs b/ethcore/light/src/net/tests/mod.rs index 47d73aef2..6a9de1467 100644 --- a/ethcore/light/src/net/tests/mod.rs +++ b/ethcore/light/src/net/tests/mod.rs @@ -20,11 +20,11 @@ use ethcore::blockchain_info::BlockChainInfo; use ethcore::client::{EachBlockWith, TestBlockChainClient}; use ethcore::ids::BlockId; -use ethcore::transaction::PendingTransaction; +use ethcore::transaction::{Action, PendingTransaction}; use ethcore::encoded; use network::{PeerId, NodeId}; -use net::buffer_flow::FlowParams; +use net::request_credits::FlowParams; use net::context::IoContext; use net::status::{Capabilities, Status, write_handshake}; use net::{encode_request, LightProtocol, Params, packet, Peer}; @@ -32,7 +32,7 @@ use provider::Provider; use request::{self, Request, Headers}; use rlp::*; -use util::{Bytes, H256, U256}; +use util::{Address, Bytes, DBValue, H256, U256}; use std::sync::Arc; @@ -127,6 +127,10 @@ impl Provider for TestProvider { None } + fn transaction_proof(&self, _req: request::TransactionProof) -> Option<Vec<DBValue>> { + None + } + fn ready_transactions(&self) -> Vec<PendingTransaction> { self.0.client.ready_transactions() } @@ -203,7 +207,7 @@ fn genesis_mismatch() { } #[test] -fn buffer_overflow() { +fn credit_overflow() { let flow_params = make_flow_params(); let capabilities = capabilities(); @@ -268,11 +272,11 @@ fn get_block_headers() { let headers: Vec<_> = (0..10).map(|i| provider.client.block_header(BlockId::Number(i + 1)).unwrap()).collect(); assert_eq!(headers.len(), 10); - let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Headers, 10); + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::Headers, 10); let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf).begin_list(10); + response_stream.append(&req_id).append(&new_creds).begin_list(10); for header in headers { response_stream.append_raw(&header.into_inner(), 1); } @@ -317,11 +321,11 @@ fn get_block_bodies() { let bodies: Vec<_> = (0..10).map(|i| provider.client.block_body(BlockId::Number(i + 1)).unwrap()).collect(); assert_eq!(bodies.len(), 10); - let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Bodies, 10); + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::Bodies, 10); let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf).begin_list(10); + response_stream.append(&req_id).append(&new_creds).begin_list(10); for body in bodies { response_stream.append_raw(&body.into_inner(), 1); } @@ -371,11 +375,11 @@ fn get_block_receipts() { .map(|hash| provider.client.block_receipts(hash).unwrap()) .collect(); - let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Receipts, receipts.len()); + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::Receipts, receipts.len()); let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf).begin_list(receipts.len()); + response_stream.append(&req_id).append(&new_creds).begin_list(receipts.len()); for block_receipts in receipts { response_stream.append_raw(&block_receipts, 1); } @@ -420,11 +424,11 @@ fn get_state_proofs() { vec![::util::sha3::SHA3_NULL_RLP.to_vec()], ]; - let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::StateProofs, 2); + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::StateProofs, 2); let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf).begin_list(2); + response_stream.append(&req_id).append(&new_creds).begin_list(2); for proof in proofs { response_stream.begin_list(proof.len()); for node in proof { @@ -472,11 +476,11 @@ fn get_contract_code() { key2.iter().chain(key2.iter()).cloned().collect(), ]; - let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Codes, 2); + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::Codes, 2); let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf).begin_list(2); + response_stream.append(&req_id).append(&new_creds).begin_list(2); for code in codes { response_stream.append(&code); } @@ -488,6 +492,56 @@ fn get_contract_code() { proto.handle_packet(&expected, &1, packet::GET_CONTRACT_CODES, &request_body); } +#[test] +fn proof_of_execution() { + let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into()); + let capabilities = capabilities(); + + let (provider, proto) = setup(flow_params.clone(), capabilities.clone()); + + let cur_status = status(provider.client.chain_info()); + + { + let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params)); + proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body.clone())); + proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &packet_body); + } + + let req_id = 112; + let mut request = Request::TransactionProof (request::TransactionProof { + at: H256::default(), + from: Address::default(), + action: Action::Call(Address::default()), + gas: 100.into(), + gas_price: 0.into(), + value: 0.into(), + data: Vec::new(), + }); + + // first: a valid amount to request execution of. + let request_body = encode_request(&request, req_id); + let response = { + let new_creds = *flow_params.limit() - flow_params.compute_cost(request::Kind::TransactionProof, 100); + + let mut response_stream = RlpStream::new_list(3); + response_stream.append(&req_id).append(&new_creds).begin_list(0); + + response_stream.out() + }; + + let expected = Expect::Respond(packet::TRANSACTION_PROOF, response); + proto.handle_packet(&expected, &1, packet::GET_TRANSACTION_PROOF, &request_body); + + // next: way too much requested gas. + if let Request::TransactionProof(ref mut req) = request { + req.gas = 100_000_000.into(); + } + let req_id = 113; + let request_body = encode_request(&request, req_id); + let expected = Expect::Punish(1); + proto.handle_packet(&expected, &1, packet::GET_TRANSACTION_PROOF, &request_body); +} + #[test] fn id_guard() { use super::request_set::RequestSet; @@ -515,10 +569,10 @@ fn id_guard() { pending_requests.insert(req_id_2, req, ::time::SteadyTime::now()); proto.peers.write().insert(peer_id, ::util::Mutex::new(Peer { - local_buffer: flow_params.create_buffer(), + local_credits: flow_params.create_credits(), status: status(provider.client.chain_info()), capabilities: capabilities.clone(), - remote_flow: Some((flow_params.create_buffer(), flow_params)), + remote_flow: Some((flow_params.create_credits(), flow_params)), sent_head: provider.client.chain_info().best_block_hash, last_update: ::time::SteadyTime::now(), pending_requests: pending_requests, diff --git a/ethcore/light/src/on_demand/mod.rs b/ethcore/light/src/on_demand/mod.rs index ec3b758ce..25cde402b 100644 --- a/ethcore/light/src/on_demand/mod.rs +++ b/ethcore/light/src/on_demand/mod.rs @@ -24,12 +24,14 @@ use std::sync::Arc; use ethcore::basic_account::BasicAccount; use ethcore::encoded; use ethcore::receipt::Receipt; +use ethcore::state::ProvedExecution; +use ethcore::executed::{Executed, ExecutionError}; use futures::{Async, Poll, Future}; use futures::sync::oneshot::{self, Sender, Receiver}; use network::PeerId; use rlp::{RlpStream, Stream}; -use util::{Bytes, RwLock, Mutex, U256}; +use util::{Bytes, DBValue, RwLock, Mutex, U256}; use util::sha3::{SHA3_NULL_RLP, SHA3_EMPTY_LIST_RLP}; use net::{Handler, Status, Capabilities, Announcement, EventContext, BasicContext, ReqId}; @@ -59,6 +61,7 @@ enum Pending { BlockReceipts(request::BlockReceipts, Sender<Vec<Receipt>>), Account(request::Account, Sender<BasicAccount>), Code(request::Code, Sender<Bytes>), + TxProof(request::TransactionProof, Sender<Result<Executed, ExecutionError>>), } /// On demand request service. See module docs for more details. @@ -418,6 +421,50 @@ impl OnDemand { self.orphaned_requests.write().push(pending) } + /// Request proof-of-execution for a transaction. + pub fn transaction_proof(&self, ctx: &BasicContext, req: request::TransactionProof) -> Receiver<Result<Executed, ExecutionError>> { + let (sender, receiver) = oneshot::channel(); + + self.dispatch_transaction_proof(ctx, req, sender); + + receiver + } + + fn dispatch_transaction_proof(&self, ctx: &BasicContext, req: request::TransactionProof, sender: Sender<Result<Executed, ExecutionError>>) { + let num = req.header.number(); + let les_req = LesRequest::TransactionProof(les_request::TransactionProof { + at: req.header.hash(), + from: req.tx.sender(), + gas: req.tx.gas, + gas_price: req.tx.gas_price, + action: req.tx.action.clone(), + value: req.tx.value, + data: req.tx.data.clone(), + }); + let pending = Pending::TxProof(req, sender); + + // we're looking for a peer with serveStateSince(num) + for (id, peer) in self.peers.read().iter() { + if peer.capabilities.serve_state_since.as_ref().map_or(false, |x| *x >= num) { + match ctx.request_from(*id, les_req.clone()) { + Ok(req_id) => { + trace!(target: "on_demand", "Assigning request to peer {}", id); + self.pending_requests.write().insert( + req_id, + pending + ); + return + } + Err(e) => + trace!(target: "on_demand", "Failed to make request of peer {}: {:?}", id, e), + } + } + } + + trace!(target: "on_demand", "No suitable peer for request"); + self.orphaned_requests.write().push(pending) + } + // dispatch orphaned requests, and discard those for which the corresponding // receiver has been dropped. fn dispatch_orphaned(&self, ctx: &BasicContext) { @@ -468,6 +515,8 @@ impl OnDemand { if !check_hangup(&mut sender) { self.dispatch_account(ctx, req, sender) }, Pending::Code(req, mut sender) => if !check_hangup(&mut sender) { self.dispatch_code(ctx, req, sender) }, + Pending::TxProof(req, mut sender) => + if !check_hangup(&mut sender) { self.dispatch_transaction_proof(ctx, req, sender) } } } } @@ -690,6 +739,36 @@ impl Handler for OnDemand { } } + fn on_transaction_proof(&self, ctx: &EventContext, req_id: ReqId, items: &[DBValue]) { + let peer = ctx.peer(); + let req = match self.pending_requests.write().remove(&req_id) { + Some(req) => req, + None => return, + }; + + match req { + Pending::TxProof(req, sender) => { + match req.check_response(items) { + ProvedExecution::Complete(executed) => { + sender.complete(Ok(executed)); + return + } + ProvedExecution::Failed(err) => { + sender.complete(Err(err)); + return + } + ProvedExecution::BadProof => { + warn!("Error handling response for transaction proof request"); + ctx.disable_peer(peer); + } + } + + self.dispatch_transaction_proof(ctx.as_basic(), req, sender); + } + _ => panic!("Only transaction proof request dispatches transaction proof requests; qed"), + } + } + fn tick(&self, ctx: &BasicContext) { self.dispatch_orphaned(ctx) } diff --git a/ethcore/light/src/on_demand/request.rs b/ethcore/light/src/on_demand/request.rs index 3964137d9..3a72db51d 100644 --- a/ethcore/light/src/on_demand/request.rs +++ b/ethcore/light/src/on_demand/request.rs @@ -16,12 +16,18 @@ //! Request types, verification, and verification errors. +use std::sync::Arc; + use ethcore::basic_account::BasicAccount; use ethcore::encoded; +use ethcore::engines::Engine; +use ethcore::env_info::EnvInfo; use ethcore::receipt::Receipt; +use ethcore::state::{self, ProvedExecution}; +use ethcore::transaction::SignedTransaction; use rlp::{RlpStream, Stream, UntrustedRlp, View}; -use util::{Address, Bytes, HashDB, H256, U256}; +use util::{Address, Bytes, DBValue, HashDB, H256, U256}; use util::memorydb::MemoryDB; use util::sha3::Hashable; use util::trie::{Trie, TrieDB, TrieError}; @@ -231,6 +237,33 @@ impl Code { } } +/// Request for transaction execution, along with the parts necessary to verify the proof. +pub struct TransactionProof { + /// The transaction to request proof of. + pub tx: SignedTransaction, + /// Block header. + pub header: encoded::Header, + /// Transaction environment info. + pub env_info: EnvInfo, + /// Consensus engine. + pub engine: Arc<Engine>, +} + +impl TransactionProof { + /// Check the proof, returning the proved execution or indicate that the proof was bad. + pub fn check_response(&self, state_items: &[DBValue]) -> ProvedExecution { + let root = self.header.state_root(); + + state::check_proof( + state_items, + root, + &self.tx, + &*self.engine, + &self.env_info, + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethcore/light/src/provider.rs b/ethcore/light/src/provider.rs index caade3857..3f55a6b99 100644 --- a/ethcore/light/src/provider.rs +++ b/ethcore/light/src/provider.rs @@ -24,7 +24,7 @@ use ethcore::client::{BlockChainClient, ProvingBlockChainClient}; use ethcore::transaction::PendingTransaction; use ethcore::ids::BlockId; use ethcore::encoded; -use util::{Bytes, RwLock, H256}; +use util::{Bytes, DBValue, RwLock, H256}; use cht::{self, BlockInfo}; use client::{LightChainClient, AsLightClient}; @@ -193,6 +193,10 @@ pub trait Provider: Send + Sync { /// Provide pending transactions. fn ready_transactions(&self) -> Vec<PendingTransaction>; + + /// Provide a proof-of-execution for the given transaction proof request. + /// Returns a vector of all state items necessary to execute the transaction. + fn transaction_proof(&self, req: request::TransactionProof) -> Option<Vec<DBValue>>; } // Implementation of a light client data provider for a client. @@ -283,6 +287,26 @@ impl<T: ProvingBlockChainClient + ?Sized> Provider for T { } } + fn transaction_proof(&self, req: request::TransactionProof) -> Option<Vec<DBValue>> { + use ethcore::transaction::Transaction; + + let id = BlockId::Hash(req.at); + let nonce = match self.nonce(&req.from, id.clone()) { + Some(nonce) => nonce, + None => return None, + }; + let transaction = Transaction { + nonce: nonce, + gas: req.gas, + gas_price: req.gas_price, + action: req.action, + value: req.value, + data: req.data, + }.fake_sign(req.from); + + self.prove_transaction(transaction, id) + } + fn ready_transactions(&self) -> Vec<PendingTransaction> { BlockChainClient::ready_transactions(self) } @@ -343,6 +367,10 @@ impl<L: AsLightClient + Send + Sync> Provider for LightProvider<L> { None } + fn transaction_proof(&self, _req: request::TransactionProof) -> Option<Vec<DBValue>> { + None + } + fn ready_transactions(&self) -> Vec<PendingTransaction> { let chain_info = self.chain_info(); self.txqueue.read().ready_transactions(chain_info.best_block_number, chain_info.best_block_timestamp) diff --git a/ethcore/light/src/types/les_request.rs b/ethcore/light/src/types/les_request.rs index b4940980e..dbff19eb5 100644 --- a/ethcore/light/src/types/les_request.rs +++ b/ethcore/light/src/types/les_request.rs @@ -16,7 +16,8 @@ //! LES request types. -use util::H256; +use ethcore::transaction::Action; +use util::{Address, H256, U256, Uint}; /// Either a hash or a number. #[derive(Debug, Clone, PartialEq, Eq)] @@ -134,6 +135,26 @@ pub struct HeaderProofs { pub requests: Vec<HeaderProof>, } +/// A request for proof of (simulated) transaction execution. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "ipc", binary)] +pub struct TransactionProof { + /// Block hash to request for. + pub at: H256, + /// Address to treat as the caller. + pub from: Address, + /// Action to take: either a call or a create. + pub action: Action, + /// Amount of gas to request proof-of-execution for. + pub gas: U256, + /// Price for each gas. + pub gas_price: U256, + /// Value to simulate sending. + pub value: U256, + /// Transaction data. + pub data: Vec<u8>, +} + /// Kinds of requests. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "ipc", binary)] @@ -150,6 +171,8 @@ pub enum Kind { Codes, /// Requesting header proofs (from the CHT). HeaderProofs, + /// Requesting proof of transaction execution. + TransactionProof, } /// Encompasses all possible types of requests in a single structure. @@ -168,6 +191,8 @@ pub enum Request { Codes(ContractCodes), /// Requesting header proofs. HeaderProofs(HeaderProofs), + /// Requesting proof of transaction execution. + TransactionProof(TransactionProof), } impl Request { @@ -180,10 +205,12 @@ impl Request { Request::StateProofs(_) => Kind::StateProofs, Request::Codes(_) => Kind::Codes, Request::HeaderProofs(_) => Kind::HeaderProofs, + Request::TransactionProof(_) => Kind::TransactionProof, } } /// Get the amount of requests being made. + /// In the case of `TransactionProof`, this is the amount of gas being requested. pub fn amount(&self) -> usize { match *self { Request::Headers(ref req) => req.max, @@ -192,6 +219,10 @@ impl Request { Request::StateProofs(ref req) => req.requests.len(), Request::Codes(ref req) => req.code_requests.len(), Request::HeaderProofs(ref req) => req.requests.len(), + Request::TransactionProof(ref req) => match req.gas > usize::max_value().into() { + true => usize::max_value(), + false => req.gas.low_u64() as usize, + } } } } diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index 7f209bad1..63be1da07 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -24,7 +24,7 @@ use time::precise_time_ns; // util use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock, MutexGuard, Hashable}; -use util::{journaldb, TrieFactory, Trie}; +use util::{journaldb, DBValue, TrieFactory, Trie}; use util::{U256, H256, Address, H2048, Uint, FixedHash}; use util::trie::TrieSpec; use util::kvdb::*; @@ -34,7 +34,7 @@ use io::*; use views::BlockView; use error::{ImportError, ExecutionError, CallError, BlockError, ImportResult, Error as EthcoreError}; use header::BlockNumber; -use state::{State, CleanupMode}; +use state::{self, State, CleanupMode}; use spec::Spec; use basic_types::Seal; use engines::Engine; @@ -308,18 +308,24 @@ impl Client { } /// The env info as of the best block. - fn latest_env_info(&self) -> EnvInfo { - let header = self.best_block_header(); + pub fn latest_env_info(&self) -> EnvInfo { + self.env_info(BlockId::Latest).expect("Best block header always stored; qed") + } - EnvInfo { - number: header.number(), - author: header.author(), - timestamp: header.timestamp(), - difficulty: header.difficulty(), - last_hashes: self.build_last_hashes(header.hash()), - gas_used: U256::default(), - gas_limit: header.gas_limit(), - } + /// The env info as of a given block. + /// returns `None` if the block unknown. + pub fn env_info(&self, id: BlockId) -> Option<EnvInfo> { + self.block_header(id).map(|header| { + EnvInfo { + number: header.number(), + author: header.author(), + timestamp: header.timestamp(), + difficulty: header.difficulty(), + last_hashes: self.build_last_hashes(header.parent_hash()), + gas_used: U256::default(), + gas_limit: header.gas_limit(), + } + }) } fn build_last_hashes(&self, parent_hash: H256) -> Arc<LastHashes> { @@ -874,17 +880,9 @@ impl snapshot::DatabaseRestore for Client { impl BlockChainClient for Client { fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError> { - let header = self.block_header(block).ok_or(CallError::StatePruned)?; - let last_hashes = self.build_last_hashes(header.parent_hash()); - let env_info = EnvInfo { - number: header.number(), - author: header.author(), - timestamp: header.timestamp(), - difficulty: header.difficulty(), - last_hashes: last_hashes, - gas_used: U256::zero(), - gas_limit: U256::max_value(), - }; + let mut env_info = self.env_info(block).ok_or(CallError::StatePruned)?; + env_info.gas_limit = U256::max_value(); + // that's just a copy of the state. let mut state = self.state_at(block).ok_or(CallError::StatePruned)?; let original_state = if analytics.state_diffing { Some(state.clone()) } else { None }; @@ -910,17 +908,13 @@ impl BlockChainClient for Client { fn estimate_gas(&self, t: &SignedTransaction, block: BlockId) -> Result<U256, CallError> { const UPPER_CEILING: u64 = 1_000_000_000_000u64; - let header = self.block_header(block).ok_or(CallError::StatePruned)?; - let last_hashes = self.build_last_hashes(header.parent_hash()); - let env_info = EnvInfo { - number: header.number(), - author: header.author(), - timestamp: header.timestamp(), - difficulty: header.difficulty(), - last_hashes: last_hashes, - gas_used: U256::zero(), - gas_limit: UPPER_CEILING.into(), + let (mut upper, env_info) = { + let mut env_info = self.env_info(block).ok_or(CallError::StatePruned)?; + let initial_upper = env_info.gas_limit; + env_info.gas_limit = UPPER_CEILING.into(); + (initial_upper, env_info) }; + // that's just a copy of the state. let original_state = self.state_at(block).ok_or(CallError::StatePruned)?; let sender = t.sender(); @@ -946,7 +940,6 @@ impl BlockChainClient for Client { .unwrap_or(false)) }; - let mut upper = header.gas_limit(); if !cond(upper)? { // impossible at block gas limit - try `UPPER_CEILING` instead. // TODO: consider raising limit by powers of two. @@ -989,7 +982,7 @@ impl BlockChainClient for Client { fn replay(&self, id: TransactionId, analytics: CallAnalytics) -> Result<Executed, CallError> { let address = self.transaction_address(id).ok_or(CallError::TransactionNotFound)?; - let header = self.block_header(BlockId::Hash(address.block_hash)).ok_or(CallError::StatePruned)?; + let mut env_info = self.env_info(BlockId::Hash(address.block_hash)).ok_or(CallError::StatePruned)?; let body = self.block_body(BlockId::Hash(address.block_hash)).ok_or(CallError::StatePruned)?; let mut state = self.state_at_beginning(BlockId::Hash(address.block_hash)).ok_or(CallError::StatePruned)?; let mut txs = body.transactions(); @@ -999,16 +992,6 @@ impl BlockChainClient for Client { } let options = TransactOptions { tracing: analytics.transaction_tracing, vm_tracing: analytics.vm_tracing, check_nonce: false }; - let last_hashes = self.build_last_hashes(header.hash()); - let mut env_info = EnvInfo { - number: header.number(), - author: header.author(), - timestamp: header.timestamp(), - difficulty: header.difficulty(), - last_hashes: last_hashes, - gas_used: U256::default(), - gas_limit: header.gas_limit(), - }; const PROOF: &'static str = "Transactions fetched from blockchain; blockchain transactions are valid; qed"; let rest = txs.split_off(address.index); for t in txs { @@ -1620,6 +1603,25 @@ impl ::client::ProvingBlockChainClient for Client { .and_then(|x| x) .unwrap_or_else(Vec::new) } + + fn prove_transaction(&self, transaction: SignedTransaction, id: BlockId) -> Option<Vec<DBValue>> { + let (state, env_info) = match (self.state_at(id), self.env_info(id)) { + (Some(s), Some(e)) => (s, e), + _ => return None, + }; + let mut jdb = self.state_db.lock().journal_db().boxed_clone(); + let backend = state::backend::Proving::new(jdb.as_hashdb_mut()); + + let mut state = state.replace_backend(backend); + let options = TransactOptions { tracing: false, vm_tracing: false, check_nonce: false }; + let res = Executive::new(&mut state, &env_info, &*self.engine, &self.factories.vm).transact(&transaction, options); + + match res { + Err(ExecutionError::Internal(_)) => return None, + _ => return Some(state.drop().1.extract_proof()), + } + } + } impl Drop for Client { diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index dc9cb5944..5d436f4c5 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -765,6 +765,10 @@ impl ProvingBlockChainClient for TestBlockChainClient { fn code_by_hash(&self, _: H256, _: BlockId) -> Bytes { Vec::new() } + + fn prove_transaction(&self, _: SignedTransaction, _: BlockId) -> Option<Vec<DBValue>> { + None + } } impl EngineClient for TestBlockChainClient { diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index 6e1ea9d31..4af20de0f 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -16,6 +16,7 @@ use std::collections::BTreeMap; use util::{U256, Address, H256, H2048, Bytes, Itertools}; +use util::hashdb::DBValue; use blockchain::TreeRoute; use verification::queue::QueueInfo as BlockQueueInfo; use block::{OpenBlock, SealedBlock}; @@ -321,4 +322,7 @@ pub trait ProvingBlockChainClient: BlockChainClient { /// Get code by address hash. fn code_by_hash(&self, account_key: H256, id: BlockId) -> Bytes; + + /// Prove execution of a transaction at the given block. + fn prove_transaction(&self, transaction: SignedTransaction, id: BlockId) -> Option<Vec<DBValue>>; } diff --git a/ethcore/src/env_info.rs b/ethcore/src/env_info.rs index 9e1bb6a40..cc42008d5 100644 --- a/ethcore/src/env_info.rs +++ b/ethcore/src/env_info.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +//! Environment information for transaction execution. + use std::cmp; use std::sync::Arc; use util::{U256, Address, H256, Hashable}; @@ -25,7 +27,7 @@ use ethjson; pub type LastHashes = Vec<H256>; /// Information concerning the execution environment for a message-call/contract-creation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EnvInfo { /// The block number. pub number: BlockNumber, diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index 2d3307b66..a78e2120f 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -79,7 +79,6 @@ //! cargo build --release //! ``` - extern crate ethcore_io as io; extern crate rustc_serialize; extern crate crypto; @@ -141,12 +140,12 @@ pub mod action_params; pub mod db; pub mod verification; pub mod state; +pub mod env_info; #[macro_use] pub mod evm; mod cache_manager; mod blooms; mod basic_types; -mod env_info; mod pod_account; mod state_db; mod account_db; diff --git a/ethcore/src/state/backend.rs b/ethcore/src/state/backend.rs index 81a770fe7..5ab620b0e 100644 --- a/ethcore/src/state/backend.rs +++ b/ethcore/src/state/backend.rs @@ -21,10 +21,12 @@ //! should become general over time to the point where not even a //! merkle trie is strictly necessary. +use std::collections::{HashSet, HashMap}; use std::sync::Arc; use state::Account; -use util::{Address, AsHashDB, HashDB, H256}; +use util::{Address, MemoryDB, Mutex, H256}; +use util::hashdb::{AsHashDB, HashDB, DBValue}; /// State backend. See module docs for more details. pub trait Backend: Send { @@ -64,21 +66,48 @@ pub trait Backend: Send { fn is_known_null(&self, address: &Address) -> bool; } -/// A raw backend which simply wraps a hashdb and does no caching. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NoCache<T>(T); +/// A raw backend used to check proofs of execution. +/// +/// This doesn't delete anything since execution proofs won't have mangled keys +/// and we want to avoid collisions. +// TODO: when account lookup moved into backends, this won't rely as tenuously on intended +// usage. +#[derive(Clone, PartialEq)] +pub struct ProofCheck(MemoryDB); -impl<T> NoCache<T> { - /// Create a new `NoCache` backend. - pub fn new(inner: T) -> Self { NoCache(inner) } - - /// Consume the backend, yielding the inner database. - pub fn into_inner(self) -> T { self.0 } +impl ProofCheck { + /// Create a new `ProofCheck` backend from the given state items. + pub fn new(proof: &[DBValue]) -> Self { + let mut db = MemoryDB::new(); + for item in proof { db.insert(item); } + ProofCheck(db) + } } -impl<T: AsHashDB + Send> Backend for NoCache<T> { - fn as_hashdb(&self) -> &HashDB { self.0.as_hashdb() } - fn as_hashdb_mut(&mut self) -> &mut HashDB { self.0.as_hashdb_mut() } +impl HashDB for ProofCheck { + fn keys(&self) -> HashMap<H256, i32> { self.0.keys() } + fn get(&self, key: &H256) -> Option<DBValue> { + self.0.get(key) + } + + fn contains(&self, key: &H256) -> bool { + self.0.contains(key) + } + + fn insert(&mut self, value: &[u8]) -> H256 { + self.0.insert(value) + } + + fn emplace(&mut self, key: H256, value: DBValue) { + self.0.emplace(key, value) + } + + fn remove(&mut self, _key: &H256) { } +} + +impl Backend for ProofCheck { + fn as_hashdb(&self) -> &HashDB { self } + fn as_hashdb_mut(&mut self) -> &mut HashDB { self } fn add_to_account_cache(&mut self, _addr: Address, _data: Option<Account>, _modified: bool) {} fn cache_code(&self, _hash: H256, _code: Arc<Vec<u8>>) {} fn get_cached_account(&self, _addr: &Address) -> Option<Option<Account>> { None } @@ -91,3 +120,104 @@ impl<T: AsHashDB + Send> Backend for NoCache<T> { fn note_non_null_account(&self, _address: &Address) {} fn is_known_null(&self, _address: &Address) -> bool { false } } + +/// Proving state backend. +/// This keeps track of all state values loaded during usage of this backend. +/// The proof-of-execution can be extracted with `extract_proof`. +/// +/// This doesn't cache anything or rely on the canonical state caches. +pub struct Proving<H: AsHashDB> { + base: H, // state we're proving values from. + changed: MemoryDB, // changed state via insertions. + proof: Mutex<HashSet<DBValue>>, +} + +impl<H: AsHashDB + Send + Sync> HashDB for Proving<H> { + fn keys(&self) -> HashMap<H256, i32> { + let mut keys = self.base.as_hashdb().keys(); + keys.extend(self.changed.keys()); + keys + } + + fn get(&self, key: &H256) -> Option<DBValue> { + match self.base.as_hashdb().get(key) { + Some(val) => { + self.proof.lock().insert(val.clone()); + Some(val) + } + None => self.changed.get(key) + } + } + + fn contains(&self, key: &H256) -> bool { + self.get(key).is_some() + } + + fn insert(&mut self, value: &[u8]) -> H256 { + self.changed.insert(value) + } + + fn emplace(&mut self, key: H256, value: DBValue) { + self.changed.emplace(key, value) + } + + fn remove(&mut self, key: &H256) { + // only remove from `changed` + if self.changed.contains(key) { + self.changed.remove(key) + } + } +} + +impl<H: AsHashDB + Send + Sync> Backend for Proving<H> { + fn as_hashdb(&self) -> &HashDB { + self + } + + fn as_hashdb_mut(&mut self) -> &mut HashDB { + self + } + + fn add_to_account_cache(&mut self, _: Address, _: Option<Account>, _: bool) { } + + fn cache_code(&self, _: H256, _: Arc<Vec<u8>>) { } + + fn get_cached_account(&self, _: &Address) -> Option<Option<Account>> { None } + + fn get_cached<F, U>(&self, _: &Address, _: F) -> Option<U> + where F: FnOnce(Option<&mut Account>) -> U + { + None + } + + fn get_cached_code(&self, _: &H256) -> Option<Arc<Vec<u8>>> { None } + fn note_non_null_account(&self, _: &Address) { } + fn is_known_null(&self, _: &Address) -> bool { false } +} + +impl<H: AsHashDB> Proving<H> { + /// Create a new `Proving` over a base database. + /// This will store all values ever fetched from that base. + pub fn new(base: H) -> Self { + Proving { + base: base, + changed: MemoryDB::new(), + proof: Mutex::new(HashSet::new()), + } + } + + /// Consume the backend, extracting the gathered proof. + pub fn extract_proof(self) -> Vec<DBValue> { + self.proof.into_inner().into_iter().collect() + } +} + +impl<H: AsHashDB + Clone> Clone for Proving<H> { + fn clone(&self) -> Self { + Proving { + base: self.base.clone(), + changed: self.changed.clone(), + proof: Mutex::new(self.proof.lock().clone()), + } + } +} diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index ebac907aa..3c5a3bc09 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -31,6 +31,7 @@ use factory::Factories; use trace::FlatTrace; use pod_account::*; use pod_state::{self, PodState}; +use types::executed::{Executed, ExecutionError}; use types::state_diff::StateDiff; use transaction::SignedTransaction; use state_db::StateDB; @@ -60,6 +61,17 @@ pub struct ApplyOutcome { /// Result type for the execution ("application") of a transaction. pub type ApplyResult = Result<ApplyOutcome, Error>; +/// Return type of proof validity check. +#[derive(Debug, Clone)] +pub enum ProvedExecution { + /// Proof wasn't enough to complete execution. + BadProof, + /// The transaction failed, but not due to a bad proof. + Failed(ExecutionError), + /// The transaction successfully completd with the given proof. + Complete(Executed), +} + #[derive(Eq, PartialEq, Clone, Copy, Debug)] /// Account modification state. Used to check if the account was /// Modified in between commits and overall. @@ -150,6 +162,39 @@ impl AccountEntry { } } +/// Check the given proof of execution. +/// `Err(ExecutionError::Internal)` indicates failure, everything else indicates +/// a successful proof (as the transaction itself may be poorly chosen). +pub fn check_proof( + proof: &[::util::DBValue], + root: H256, + transaction: &SignedTransaction, + engine: &Engine, + env_info: &EnvInfo, +) -> ProvedExecution { + let backend = self::backend::ProofCheck::new(proof); + let mut factories = Factories::default(); + factories.accountdb = ::account_db::Factory::Plain; + + let res = State::from_existing( + backend, + root, + engine.account_start_nonce(), + factories + ); + + let mut state = match res { + Ok(state) => state, + Err(_) => return ProvedExecution::BadProof, + }; + + match state.execute(env_info, engine, transaction, false) { + Ok(executed) => ProvedExecution::Complete(executed), + Err(ExecutionError::Internal(_)) => ProvedExecution::BadProof, + Err(e) => ProvedExecution::Failed(e), + } +} + /// Representation of the entire state of all accounts in the system. /// /// `State` can work together with `StateDB` to share account cache. @@ -264,6 +309,19 @@ impl<B: Backend> State<B> { Ok(state) } + /// Swap the current backend for another. + // TODO: [rob] find a less hacky way to avoid duplication of `Client::state_at`. + pub fn replace_backend<T: Backend>(self, backend: T) -> State<T> { + State { + db: backend, + root: self.root, + cache: self.cache, + checkpoints: self.checkpoints, + account_start_nonce: self.account_start_nonce, + factories: self.factories, + } + } + /// Create a recoverable checkpoint of this state. pub fn checkpoint(&mut self) { self.checkpoints.get_mut().push(HashMap::new()); @@ -535,16 +593,12 @@ impl<B: Backend> State<B> { Ok(()) } - /// Execute a given transaction. + /// Execute a given transaction, producing a receipt and an optional trace. /// This will change the state accordingly. pub fn apply(&mut self, env_info: &EnvInfo, engine: &Engine, t: &SignedTransaction, tracing: bool) -> ApplyResult { // let old = self.to_pod(); - let options = TransactOptions { tracing: tracing, vm_tracing: false, check_nonce: true }; - let vm_factory = self.factories.vm.clone(); - let e = Executive::new(self, env_info, engine, &vm_factory).transact(t, options)?; - - // TODO uncomment once to_pod() works correctly. + let e = self.execute(env_info, engine, t, tracing)?; // trace!("Applied transaction. Diff:\n{}\n", state_diff::diff_pod(&old, &self.to_pod())); let state_root = if env_info.number < engine.params().eip98_transition { self.commit()?; @@ -557,6 +611,15 @@ impl<B: Backend> State<B> { Ok(ApplyOutcome{receipt: receipt, trace: e.trace}) } + // Execute a given transaction. + fn execute(&mut self, env_info: &EnvInfo, engine: &Engine, t: &SignedTransaction, tracing: bool) -> Result<Executed, ExecutionError> { + let options = TransactOptions { tracing: tracing, vm_tracing: false, check_nonce: true }; + let vm_factory = self.factories.vm.clone(); + + Executive::new(self, env_info, engine, &vm_factory).transact(t, options) + } + + /// Commit accounts to SecTrieDBMut. This is similar to cpp-ethereum's dev::eth::commit. /// `accounts` is mutable because we may need to commit the code or storage and record that. #[cfg_attr(feature="dev", allow(match_ref_pats))] diff --git a/ethcore/src/tests/client.rs b/ethcore/src/tests/client.rs index 526738586..3734c5520 100644 --- a/ethcore/src/tests/client.rs +++ b/ethcore/src/tests/client.rs @@ -16,7 +16,8 @@ use io::IoChannel; use client::{BlockChainClient, MiningBlockChainClient, Client, ClientConfig, BlockId}; -use state::CleanupMode; +use state::{self, State, CleanupMode}; +use executive::Executive; use ethereum; use block::IsBlock; use tests::helpers::*; @@ -341,3 +342,43 @@ fn does_not_propagate_delayed_transactions() { assert_eq!(2, client.ready_transactions().len()); assert_eq!(2, client.miner().pending_transactions().len()); } + +#[test] +fn transaction_proof() { + use ::client::ProvingBlockChainClient; + + let client_result = generate_dummy_client(0); + let client = client_result.reference(); + let address = Address::random(); + let test_spec = Spec::new_test(); + for _ in 0..20 { + let mut b = client.prepare_open_block(Address::default(), (3141562.into(), 31415620.into()), vec![]); + b.block_mut().fields_mut().state.add_balance(&address, &5.into(), CleanupMode::NoEmpty).unwrap(); + b.block_mut().fields_mut().state.commit().unwrap(); + let b = b.close_and_lock().seal(&*test_spec.engine, vec![]).unwrap(); + client.import_sealed_block(b).unwrap(); // account change is in the journal overlay + } + + let transaction = Transaction { + nonce: 0.into(), + gas_price: 0.into(), + gas: 21000.into(), + action: Action::Call(Address::default()), + value: 5.into(), + data: Vec::new(), + }.fake_sign(address); + + let proof = client.prove_transaction(transaction.clone(), BlockId::Latest).unwrap(); + let backend = state::backend::ProofCheck::new(&proof); + + let mut factories = ::factory::Factories::default(); + factories.accountdb = ::account_db::Factory::Plain; // raw state values, no mangled keys. + let root = client.best_block_header().state_root(); + + let mut state = State::from_existing(backend, root, 0.into(), factories.clone()).unwrap(); + Executive::new(&mut state, &client.latest_env_info(), &*test_spec.engine, &factories.vm) + .transact(&transaction, Default::default()).unwrap(); + + assert_eq!(state.balance(&Address::default()).unwrap(), 5.into()); + assert_eq!(state.balance(&address).unwrap(), 95.into()); +} diff --git a/rpc/src/v1/helpers/dispatch.rs b/rpc/src/v1/helpers/dispatch.rs index 0bea7f9a1..b11ada048 100644 --- a/rpc/src/v1/helpers/dispatch.rs +++ b/rpc/src/v1/helpers/dispatch.rs @@ -158,6 +158,54 @@ impl<C: MiningBlockChainClient, M: MinerService> Dispatcher for FullDispatcher<C } } +/// Get a recent gas price corpus. +// TODO: this could be `impl Trait`. +pub fn fetch_gas_price_corpus( + sync: Arc<LightSync>, + client: Arc<LightChainClient>, + on_demand: Arc<OnDemand>, + cache: Arc<Mutex<LightDataCache>>, +) -> BoxFuture<Corpus<U256>, Error> { + const GAS_PRICE_SAMPLE_SIZE: usize = 100; + + if let Some(cached) = cache.lock().gas_price_corpus() { + return future::ok(cached).boxed() + } + + let cache = cache.clone(); + let eventual_corpus = sync.with_context(|ctx| { + // get some recent headers with gas used, + // and request each of the blocks from the network. + let block_futures = client.ancestry_iter(BlockId::Latest) + .filter(|hdr| hdr.gas_used() != U256::default()) + .take(GAS_PRICE_SAMPLE_SIZE) + .map(request::Body::new) + .map(|req| on_demand.block(ctx, req)); + + // as the blocks come in, collect gas prices into a vector + stream::futures_unordered(block_futures) + .fold(Vec::new(), |mut v, block| { + for t in block.transaction_views().iter() { + v.push(t.gas_price()) + } + + future::ok(v) + }) + .map(move |v| { + // produce a corpus from the vector, cache it, and return + // the median as the intended gas price. + let corpus: ::stats::Corpus<_> = v.into(); + cache.lock().set_gas_price_corpus(corpus.clone()); + corpus + }) + }); + + match eventual_corpus { + Some(corp) => corp.map_err(|_| errors::no_light_peers()).boxed(), + None => future::err(errors::network_disabled()).boxed(), + } +} + /// Dispatcher for light clients -- fetches default gas price, next nonce, etc. from network. /// Light client `ETH` RPC. #[derive(Clone)] @@ -197,44 +245,12 @@ impl LightDispatcher { /// Get a recent gas price corpus. // TODO: this could be `impl Trait`. pub fn gas_price_corpus(&self) -> BoxFuture<Corpus<U256>, Error> { - const GAS_PRICE_SAMPLE_SIZE: usize = 100; - - if let Some(cached) = self.cache.lock().gas_price_corpus() { - return future::ok(cached).boxed() - } - - let cache = self.cache.clone(); - let eventual_corpus = self.sync.with_context(|ctx| { - // get some recent headers with gas used, - // and request each of the blocks from the network. - let block_futures = self.client.ancestry_iter(BlockId::Latest) - .filter(|hdr| hdr.gas_used() != U256::default()) - .take(GAS_PRICE_SAMPLE_SIZE) - .map(request::Body::new) - .map(|req| self.on_demand.block(ctx, req)); - - // as the blocks come in, collect gas prices into a vector - stream::futures_unordered(block_futures) - .fold(Vec::new(), |mut v, block| { - for t in block.transaction_views().iter() { - v.push(t.gas_price()) - } - - future::ok(v) - }) - .map(move |v| { - // produce a corpus from the vector, cache it, and return - // the median as the intended gas price. - let corpus: ::stats::Corpus<_> = v.into(); - cache.lock().set_gas_price_corpus(corpus.clone()); - corpus - }) - }); - - match eventual_corpus { - Some(corp) => corp.map_err(|_| errors::no_light_peers()).boxed(), - None => future::err(errors::network_disabled()).boxed(), - } + fetch_gas_price_corpus( + self.sync.clone(), + self.client.clone(), + self.on_demand.clone(), + self.cache.clone(), + ) } /// Get an account's next nonce. @@ -285,7 +301,12 @@ impl Dispatcher for LightDispatcher { // fast path for known gas price. match request_gas_price { Some(gas_price) => future::ok(with_gas_price(gas_price)).boxed(), - None => self.gas_price_corpus().and_then(|corp| match corp.median() { + None => fetch_gas_price_corpus( + self.sync.clone(), + self.client.clone(), + self.on_demand.clone(), + self.cache.clone() + ).and_then(|corp| match corp.median() { Some(median) => future::ok(*median), None => future::ok(DEFAULT_GAS_PRICE), // fall back to default on error. }).map(with_gas_price).boxed() diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 6a1702a44..6df8f5278 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -632,26 +632,34 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where self.send_raw_transaction(raw) } - fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> { + fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> BoxFuture<Bytes, Error> { let request = CallRequest::into(request); - let signed = self.sign_call(request)?; - - let result = match num.0 { - BlockNumber::Pending => take_weak!(self.miner).call(&*take_weak!(self.client), &signed, Default::default()), - num => take_weak!(self.client).call(&signed, num.into(), Default::default()), + let signed = match self.sign_call(request) { + Ok(signed) => signed, + Err(e) => return future::err(e).boxed(), }; - result + let result = match num.0 { + BlockNumber::Pending => take_weakf!(self.miner).call(&*take_weakf!(self.client), &signed, Default::default()), + num => take_weakf!(self.client).call(&signed, num.into(), Default::default()), + }; + + future::done(result .map(|b| b.output.into()) .map_err(errors::from_call_error) + ).boxed() } - fn estimate_gas(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> { + fn estimate_gas(&self, request: CallRequest, num: Trailing<BlockNumber>) -> BoxFuture<RpcU256, Error> { let request = CallRequest::into(request); - let signed = self.sign_call(request)?; - take_weak!(self.client).estimate_gas(&signed, num.0.into()) + let signed = match self.sign_call(request) { + Ok(signed) => signed, + Err(e) => return future::err(e).boxed(), + }; + future::done(take_weakf!(self.client).estimate_gas(&signed, num.0.into()) .map(Into::into) .map_err(errors::from_call_error) + ).boxed() } fn compile_lll(&self, _: String) -> Result<Bytes, Error> { diff --git a/rpc/src/v1/impls/light/eth.rs b/rpc/src/v1/impls/light/eth.rs index 6251b67fc..f889faf00 100644 --- a/rpc/src/v1/impls/light/eth.rs +++ b/rpc/src/v1/impls/light/eth.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use jsonrpc_core::Error; use jsonrpc_macros::Trailing; +use light::cache::Cache as LightDataCache; use light::client::Client as LightClient; use light::{cht, TransactionQueue}; use light::on_demand::{request, OnDemand}; @@ -31,17 +32,18 @@ use light::on_demand::{request, OnDemand}; use ethcore::account_provider::{AccountProvider, DappId}; use ethcore::basic_account::BasicAccount; use ethcore::encoded; +use ethcore::executed::{Executed, ExecutionError}; use ethcore::ids::BlockId; -use ethcore::transaction::SignedTransaction; +use ethcore::transaction::{Action, SignedTransaction, Transaction as EthTransaction}; use ethsync::LightSync; use rlp::{UntrustedRlp, View}; use util::sha3::{SHA3_NULL_RLP, SHA3_EMPTY_LIST_RLP}; -use util::{RwLock, U256}; +use util::{RwLock, Mutex, FixedHash, Uint, U256}; -use futures::{future, Future, BoxFuture}; +use futures::{future, Future, BoxFuture, IntoFuture}; use futures::sync::oneshot; -use v1::helpers::{CallRequest as CRequest, errors, limit_logs}; +use v1::helpers::{CallRequest as CRequest, errors, limit_logs, dispatch}; use v1::helpers::block_import::is_major_importing; use v1::traits::Eth; use v1::types::{ @@ -60,6 +62,7 @@ pub struct EthClient { on_demand: Arc<OnDemand>, transaction_queue: Arc<RwLock<TransactionQueue>>, accounts: Arc<AccountProvider>, + cache: Arc<Mutex<LightDataCache>>, } // helper for internal error: on demand sender cancelled. @@ -67,6 +70,8 @@ fn err_premature_cancel(_cancel: oneshot::Canceled) -> Error { errors::internal("on-demand sender prematurely cancelled", "") } +type ExecutionResult = Result<Executed, ExecutionError>; + impl EthClient { /// Create a new `EthClient` with a handle to the light sync instance, client, /// and on-demand request service, which is assumed to be attached as a handler. @@ -76,6 +81,7 @@ impl EthClient { on_demand: Arc<OnDemand>, transaction_queue: Arc<RwLock<TransactionQueue>>, accounts: Arc<AccountProvider>, + cache: Arc<Mutex<LightDataCache>>, ) -> Self { EthClient { sync: sync, @@ -83,6 +89,7 @@ impl EthClient { on_demand: on_demand, transaction_queue: transaction_queue, accounts: accounts, + cache: cache, } } @@ -147,6 +154,80 @@ impl EthClient { .unwrap_or_else(|| future::err(errors::network_disabled()).boxed()) }).boxed() } + + // helper for getting proved execution. + fn proved_execution(&self, req: CallRequest, num: Trailing<BlockNumber>) -> BoxFuture<ExecutionResult, Error> { + const DEFAULT_GAS_PRICE: U256 = U256([0, 0, 0, 21_000_000]); + + + let (sync, on_demand, client) = (self.sync.clone(), self.on_demand.clone(), self.client.clone()); + let req: CRequest = req.into(); + let id = num.0.into(); + + let from = req.from.unwrap_or(Address::zero()); + let nonce_fut = match req.nonce { + Some(nonce) => future::ok(Some(nonce)).boxed(), + None => self.account(from, id).map(|acc| acc.map(|a| a.nonce)).boxed(), + }; + + let gas_price_fut = match req.gas_price { + Some(price) => future::ok(price).boxed(), + None => dispatch::fetch_gas_price_corpus( + self.sync.clone(), + self.client.clone(), + self.on_demand.clone(), + self.cache.clone(), + ).map(|corp| match corp.median() { + Some(median) => *median, + None => DEFAULT_GAS_PRICE, + }).boxed() + }; + + // if nonce resolves, this should too since it'll be in the LRU-cache. + let header_fut = self.header(id); + + // fetch missing transaction fields from the network. + nonce_fut.join(gas_price_fut).and_then(move |(nonce, gas_price)| { + let action = req.to.map_or(Action::Create, Action::Call); + let gas = req.gas.unwrap_or(U256::from(10_000_000)); // better gas amount? + let value = req.value.unwrap_or_else(U256::zero); + let data = req.data.map_or_else(Vec::new, |d| d.to_vec()); + + future::done(match nonce { + Some(n) => Ok(EthTransaction { + nonce: n, + action: action, + gas: gas, + gas_price: gas_price, + value: value, + data: data, + }.fake_sign(from)), + None => Err(errors::unknown_block()), + }) + }).join(header_fut).and_then(move |(tx, hdr)| { + // then request proved execution. + // TODO: get last-hashes from network. + let (env_info, hdr) = match (client.env_info(id), hdr) { + (Some(env_info), Some(hdr)) => (env_info, hdr), + _ => return future::err(errors::unknown_block()).boxed(), + }; + let request = request::TransactionProof { + tx: tx, + header: hdr, + env_info: env_info, + engine: client.engine().clone(), + }; + + let proved_future = sync.with_context(move |ctx| { + on_demand.transaction_proof(ctx, request).map_err(err_premature_cancel).boxed() + }); + + match proved_future { + Some(fut) => fut.boxed(), + None => future::err(errors::network_disabled()).boxed(), + } + }).boxed() + } } impl Eth for EthClient { @@ -322,12 +403,23 @@ impl Eth for EthClient { self.send_raw_transaction(raw) } - fn call(&self, req: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> { - Err(errors::unimplemented(None)) + fn call(&self, req: CallRequest, num: Trailing<BlockNumber>) -> BoxFuture<Bytes, Error> { + self.proved_execution(req, num).and_then(|res| { + match res { + Ok(exec) => Ok(exec.output.into()), + Err(e) => Err(errors::execution(e)), + } + }).boxed() } - fn estimate_gas(&self, req: CallRequest, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> { - Err(errors::unimplemented(None)) + fn estimate_gas(&self, req: CallRequest, num: Trailing<BlockNumber>) -> BoxFuture<RpcU256, Error> { + // TODO: binary chop for more accurate estimates. + self.proved_execution(req, num).and_then(|res| { + match res { + Ok(exec) => Ok((exec.refunded + exec.gas_used).into()), + Err(e) => Err(errors::execution(e)), + } + }).boxed() } fn transaction_by_hash(&self, hash: RpcH256) -> Result<Option<Transaction>, Error> { @@ -355,19 +447,20 @@ impl Eth for EthClient { } fn compilers(&self) -> Result<Vec<String>, Error> { - Err(errors::unimplemented(None)) + Err(errors::deprecated("Compilation functionality is deprecated.".to_string())) + } - fn compile_lll(&self, _code: String) -> Result<Bytes, Error> { - Err(errors::unimplemented(None)) + fn compile_lll(&self, _: String) -> Result<Bytes, Error> { + Err(errors::deprecated("Compilation of LLL via RPC is deprecated".to_string())) } - fn compile_solidity(&self, _code: String) -> Result<Bytes, Error> { - Err(errors::unimplemented(None)) + fn compile_serpent(&self, _: String) -> Result<Bytes, Error> { + Err(errors::deprecated("Compilation of Serpent via RPC is deprecated".to_string())) } - fn compile_serpent(&self, _code: String) -> Result<Bytes, Error> { - Err(errors::unimplemented(None)) + fn compile_solidity(&self, _: String) -> Result<Bytes, Error> { + Err(errors::deprecated("Compilation of Solidity via RPC is deprecated".to_string())) } fn logs(&self, _filter: Filter) -> Result<Vec<Log>, Error> { diff --git a/rpc/src/v1/traits/eth.rs b/rpc/src/v1/traits/eth.rs index eaf608c60..365ad9320 100644 --- a/rpc/src/v1/traits/eth.rs +++ b/rpc/src/v1/traits/eth.rs @@ -110,12 +110,12 @@ build_rpc_trait! { fn submit_transaction(&self, Bytes) -> Result<H256, Error>; /// Call contract, returning the output data. - #[rpc(name = "eth_call")] - fn call(&self, CallRequest, Trailing<BlockNumber>) -> Result<Bytes, Error>; + #[rpc(async, name = "eth_call")] + fn call(&self, CallRequest, Trailing<BlockNumber>) -> BoxFuture<Bytes, Error>; /// Estimate gas needed for execution of given contract. - #[rpc(name = "eth_estimateGas")] - fn estimate_gas(&self, CallRequest, Trailing<BlockNumber>) -> Result<U256, Error>; + #[rpc(async, name = "eth_estimateGas")] + fn estimate_gas(&self, CallRequest, Trailing<BlockNumber>) -> BoxFuture<U256, Error>; /// Get transaction by its hash. #[rpc(name = "eth_getTransactionByHash")] diff --git a/sync/src/light_sync/tests/test_net.rs b/sync/src/light_sync/tests/test_net.rs index b73da48bb..d0e472374 100644 --- a/sync/src/light_sync/tests/test_net.rs +++ b/sync/src/light_sync/tests/test_net.rs @@ -27,7 +27,7 @@ use ethcore::spec::Spec; use io::IoChannel; use light::client::Client as LightClient; use light::net::{LightProtocol, IoContext, Capabilities, Params as LightParams}; -use light::net::buffer_flow::FlowParams; +use light::net::request_credits::FlowParams; use network::{NodeId, PeerId}; use util::RwLock; diff --git a/util/src/hashdb.rs b/util/src/hashdb.rs index 3b1939cae..8217413ef 100644 --- a/util/src/hashdb.rs +++ b/util/src/hashdb.rs @@ -125,3 +125,13 @@ impl<T: HashDB> AsHashDB for T { self } } + +impl<'a> AsHashDB for &'a mut HashDB { + fn as_hashdb(&self) -> &HashDB { + &**self + } + + fn as_hashdb_mut(&mut self) -> &mut HashDB { + &mut **self + } +} From 98be191b25550785882342d638d8c05d18d5b2f6 Mon Sep 17 00:00:00 2001 From: keorn <pczaban@gmail.com> Date: Wed, 8 Mar 2017 14:41:24 +0100 Subject: [PATCH 89/93] Fix validator contract syncing (#4789) * make validator set aware of various states * fix updater build * clean up contract call * failing sync test * adjust tests * nicer indent [ci skip] * revert bound divisor --- ethcore/res/authority_round.json | 2 +- ethcore/res/tendermint.json | 2 +- ethcore/res/validator_contract.json | 2 +- ethcore/src/client/client.rs | 6 +- ethcore/src/client/test_client.rs | 2 +- ethcore/src/client/traits.rs | 2 +- ethcore/src/engines/authority_round.rs | 64 ++++--- ethcore/src/engines/basic_authority.rs | 26 +-- ethcore/src/engines/tendermint/mod.rs | 81 ++++---- ethcore/src/engines/validator_set/contract.rs | 37 ++-- ethcore/src/engines/validator_set/mod.rs | 12 +- .../engines/validator_set/safe_contract.rs | 174 +++++++++++------- .../src/engines/validator_set/simple_list.rs | 22 ++- .../src/miner/service_transaction_checker.rs | 3 +- updater/src/updater.rs | 4 +- 15 files changed, 260 insertions(+), 179 deletions(-) diff --git a/ethcore/res/authority_round.json b/ethcore/res/authority_round.json index ac7eb5041..dba7e28a5 100644 --- a/ethcore/res/authority_round.json +++ b/ethcore/res/authority_round.json @@ -33,7 +33,7 @@ "timestamp": "0x00", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "extraData": "0x", - "gasLimit": "0x2fefd8" + "gasLimit": "0x222222" }, "accounts": { "0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/res/tendermint.json b/ethcore/res/tendermint.json index 83372fea5..642f5b385 100644 --- a/ethcore/res/tendermint.json +++ b/ethcore/res/tendermint.json @@ -38,7 +38,7 @@ "timestamp": "0x00", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "extraData": "0x", - "gasLimit": "0x2fefd8" + "gasLimit": "0x222222" }, "accounts": { "0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/res/validator_contract.json b/ethcore/res/validator_contract.json index 33fdf4c4f..6c2f87758 100644 --- a/ethcore/res/validator_contract.json +++ b/ethcore/res/validator_contract.json @@ -27,7 +27,7 @@ "timestamp": "0x00", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "extraData": "0x", - "gasLimit": "0x2fefd8" + "gasLimit": "0x222222" }, "accounts": { "0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index 63be1da07..edd585551 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -253,7 +253,7 @@ impl Client { if let Some(reg_addr) = client.additional_params().get("registrar").and_then(|s| Address::from_str(s).ok()) { trace!(target: "client", "Found registrar at {}", reg_addr); let weak = Arc::downgrade(&client); - let registrar = Registry::new(reg_addr, move |a, d| weak.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d))); + let registrar = Registry::new(reg_addr, move |a, d| weak.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(BlockId::Latest, a, d))); *client.registrar.lock() = Some(registrar); } Ok(client) @@ -1428,7 +1428,7 @@ impl BlockChainClient for Client { } } - fn call_contract(&self, address: Address, data: Bytes) -> Result<Bytes, String> { + fn call_contract(&self, block_id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String> { let from = Address::default(); let transaction = Transaction { nonce: self.latest_nonce(&from), @@ -1439,7 +1439,7 @@ impl BlockChainClient for Client { data: data, }.fake_sign(from); - self.call(&transaction, BlockId::Latest, Default::default()) + self.call(&transaction, block_id, Default::default()) .map_err(|e| format!("{:?}", e)) .map(|executed| { executed.output diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index 5d436f4c5..6a613fa7d 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -731,7 +731,7 @@ impl BlockChainClient for TestBlockChainClient { } } - fn call_contract(&self, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) } + fn call_contract(&self, _id: BlockId, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) } fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError> { let transaction = Transaction { diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index 4af20de0f..e31712852 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -255,7 +255,7 @@ pub trait BlockChainClient : Sync + Send { fn pruning_info(&self) -> PruningInfo; /// Like `call`, but with various defaults. Designed to be used for calling contracts. - fn call_contract(&self, address: Address, data: Bytes) -> Result<Bytes, String>; + fn call_contract(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String>; /// Import a transaction: used for misbehaviour reporting. fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError>; diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 1000cfda2..45be4a491 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -141,12 +141,12 @@ impl AuthorityRound { } } - fn step_proposer(&self, step: usize) -> Address { - self.validators.get(step) + fn step_proposer(&self, bh: &H256, step: usize) -> Address { + self.validators.get(bh, step) } - fn is_step_proposer(&self, step: usize, address: &Address) -> bool { - self.step_proposer(step) == *address + fn is_step_proposer(&self, bh: &H256, step: usize, address: &Address) -> bool { + self.step_proposer(bh, step) == *address } } @@ -231,7 +231,7 @@ impl Engine for AuthorityRound { } fn seals_internally(&self) -> Option<bool> { - Some(self.validators.contains(&self.signer.address())) + Some(self.signer.address() != Address::default()) } /// Attempt to seal the block internally. @@ -242,7 +242,7 @@ impl Engine for AuthorityRound { if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; } let header = block.header(); let step = self.step.load(AtomicOrdering::SeqCst); - if self.is_step_proposer(step, header.author()) { + if self.is_step_proposer(header.parent_hash(), step, header.author()) { if let Ok(signature) = self.signer.sign(header.bare_hash()) { trace!(target: "engine", "generate_seal: Issuing a block for step {}.", step); self.proposed.store(true, AtomicOrdering::SeqCst); @@ -281,17 +281,20 @@ impl Engine for AuthorityRound { } } - /// Check if the signature belongs to the correct proposer. - fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { - let header_step = header_step(header)?; - // Give one step slack if step is lagging, double vote is still not possible. - if header_step <= self.step.load(AtomicOrdering::SeqCst) + 1 { - let proposer_signature = header_signature(header)?; - let correct_proposer = self.step_proposer(header_step); - if verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? { + fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { Ok(()) - } else { - trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", header_step); + } + + /// Do the validator and gas limit validation. + fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> { + let step = header_step(header)?; + // Give one step slack if step is lagging, double vote is still not possible. + if step <= self.step.load(AtomicOrdering::SeqCst) + 1 { + // Check if the signature belongs to a validator, can depend on parent state. + let proposer_signature = header_signature(header)?; + let correct_proposer = self.step_proposer(header.parent_hash(), step); + if !verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? { + trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", step); Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))? } } else { @@ -299,14 +302,12 @@ impl Engine for AuthorityRound { self.validators.report_benign(header.author()); Err(BlockError::InvalidSeal)? } - } - fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> { + // Do not calculate difficulty for genesis blocks. if header.number() == 0 { return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))); } - let step = header_step(header)?; // Check if parent is from a previous step. if step == header_step(parent)? { trace!(target: "engine", "Multiple blocks proposed for step {}.", step); @@ -394,7 +395,7 @@ mod tests { let mut header: Header = Header::default(); header.set_seal(vec![encode(&H520::default()).to_vec()]); - let verify_result = engine.verify_block_unordered(&header, None); + let verify_result = engine.verify_block_family(&header, &Default::default(), None); assert!(verify_result.is_err()); } @@ -432,10 +433,14 @@ mod tests { #[test] fn proposer_switching() { - let mut header: Header = Header::default(); let tap = AccountProvider::transient_provider(); let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); - + let mut parent_header: Header = Header::default(); + parent_header.set_seal(vec![encode(&0usize).to_vec()]); + parent_header.set_gas_limit(U256::from_str("222222").unwrap()); + let mut header: Header = Header::default(); + header.set_number(1); + header.set_gas_limit(U256::from_str("222222").unwrap()); header.set_author(addr); let engine = Spec::new_test_round().engine; @@ -444,17 +449,22 @@ mod tests { // Two validators. // Spec starts with step 2. header.set_seal(vec![encode(&2usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); - assert!(engine.verify_block_seal(&header).is_err()); + assert!(engine.verify_block_family(&header, &parent_header, None).is_err()); header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); - assert!(engine.verify_block_seal(&header).is_ok()); + assert!(engine.verify_block_family(&header, &parent_header, None).is_ok()); } #[test] fn rejects_future_block() { - let mut header: Header = Header::default(); let tap = AccountProvider::transient_provider(); let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); + let mut parent_header: Header = Header::default(); + parent_header.set_seal(vec![encode(&0usize).to_vec()]); + parent_header.set_gas_limit(U256::from_str("222222").unwrap()); + let mut header: Header = Header::default(); + header.set_number(1); + header.set_gas_limit(U256::from_str("222222").unwrap()); header.set_author(addr); let engine = Spec::new_test_round().engine; @@ -463,8 +473,8 @@ mod tests { // Two validators. // Spec starts with step 2. header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); - assert!(engine.verify_block_seal(&header).is_ok()); + assert!(engine.verify_block_family(&header, &parent_header, None).is_ok()); header.set_seal(vec![encode(&5usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); - assert!(engine.verify_block_seal(&header).is_err()); + assert!(engine.verify_block_family(&header, &parent_header, None).is_err()); } } diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index 50051bf7e..34b89b2d6 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -104,14 +104,14 @@ impl Engine for BasicAuthority { } fn seals_internally(&self) -> Option<bool> { - Some(self.validators.contains(&self.signer.address())) + Some(self.signer.address() != Address::default()) } /// Attempt to seal the block internally. fn generate_seal(&self, block: &ExecutedBlock) -> Seal { let header = block.header(); let author = header.author(); - if self.validators.contains(author) { + if self.validators.contains(header.parent_hash(), author) { // account should be pernamently unlocked, otherwise sealing will fail if let Ok(signature) = self.signer.sign(header.bare_hash()) { return Seal::Regular(vec![::rlp::encode(&(&H520::from(signature) as &[u8])).to_vec()]); @@ -133,20 +133,20 @@ impl Engine for BasicAuthority { Ok(()) } - fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> { - use rlp::{UntrustedRlp, View}; - - // check the signature is legit. - let sig = UntrustedRlp::new(&header.seal()[0]).as_val::<H520>()?; - let signer = public_to_address(&recover(&sig.into(), &header.bare_hash())?); - if !self.validators.contains(&signer) { - return Err(BlockError::InvalidSeal)?; - } + fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> { Ok(()) } fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> { - // we should not calculate difficulty for genesis blocks + use rlp::{UntrustedRlp, View}; + // Check if the signature belongs to a validator, can depend on parent state. + let sig = UntrustedRlp::new(&header.seal()[0]).as_val::<H520>()?; + let signer = public_to_address(&recover(&sig.into(), &header.bare_hash())?); + if !self.validators.contains(header.parent_hash(), &signer) { + return Err(BlockError::InvalidSeal)?; + } + + // Do not calculate difficulty for genesis blocks. if header.number() == 0 { return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))); } @@ -239,7 +239,7 @@ mod tests { let mut header: Header = Header::default(); header.set_seal(vec![::rlp::encode(&H520::default()).to_vec()]); - let verify_result = engine.verify_block_unordered(&header, None); + let verify_result = engine.verify_block_family(&header, &Default::default(), None); assert!(verify_result.is_err()); } diff --git a/ethcore/src/engines/tendermint/mod.rs b/ethcore/src/engines/tendermint/mod.rs index 4c825cf20..aac101447 100644 --- a/ethcore/src/engines/tendermint/mod.rs +++ b/ethcore/src/engines/tendermint/mod.rs @@ -95,6 +95,8 @@ pub struct Tendermint { last_lock: AtomicUsize, /// Bare hash of the proposed block, used for seal submission. proposal: RwLock<Option<H256>>, + /// Hash of the proposal parent block. + proposal_parent: RwLock<H256>, /// Set used to determine the current validators. validators: Box<ValidatorSet + Send + Sync>, } @@ -114,11 +116,12 @@ impl Tendermint { height: AtomicUsize::new(1), view: AtomicUsize::new(0), step: RwLock::new(Step::Propose), - votes: VoteCollector::default(), + votes: Default::default(), signer: Default::default(), lock_change: RwLock::new(None), last_lock: AtomicUsize::new(0), proposal: RwLock::new(None), + proposal_parent: Default::default(), validators: new_validator_set(our_params.validators), }); let handler = TransitionHandler::new(Arc::downgrade(&engine) as Weak<Engine>, Box::new(our_params.timeouts)); @@ -232,7 +235,7 @@ impl Tendermint { let height = self.height.load(AtomicOrdering::SeqCst); if let Some(block_hash) = *self.proposal.read() { // Generate seal and remove old votes. - if self.is_signer_proposer() { + if self.is_signer_proposer(&*self.proposal_parent.read()) { let proposal_step = VoteStep::new(height, view, Step::Propose); let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit); if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) { @@ -254,23 +257,23 @@ impl Tendermint { } fn is_authority(&self, address: &Address) -> bool { - self.validators.contains(address) + self.validators.contains(&*self.proposal_parent.read(), address) } fn is_above_threshold(&self, n: usize) -> bool { - n > self.validators.count() * 2/3 + n > self.validators.count(&*self.proposal_parent.read()) * 2/3 } /// Find the designated for the given view. - fn view_proposer(&self, height: Height, view: View) -> Address { + fn view_proposer(&self, bh: &H256, height: Height, view: View) -> Address { let proposer_nonce = height + view; trace!(target: "engine", "Proposer nonce: {}", proposer_nonce); - self.validators.get(proposer_nonce) + self.validators.get(bh, proposer_nonce) } /// Check if address is a proposer for given view. - fn is_view_proposer(&self, height: Height, view: View, address: &Address) -> Result<(), EngineError> { - let proposer = self.view_proposer(height, view); + fn is_view_proposer(&self, bh: &H256, height: Height, view: View, address: &Address) -> Result<(), EngineError> { + let proposer = self.view_proposer(bh, height, view); if proposer == *address { Ok(()) } else { @@ -279,8 +282,8 @@ impl Tendermint { } /// Check if current signer is the current proposer. - fn is_signer_proposer(&self) -> bool { - let proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst)); + fn is_signer_proposer(&self, bh: &H256) -> bool { + let proposer = self.view_proposer(bh, self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst)); self.signer.is_address(&proposer) } @@ -419,7 +422,7 @@ impl Engine for Tendermint { /// Should this node participate. fn seals_internally(&self) -> Option<bool> { - Some(self.is_authority(&self.signer.address())) + Some(self.signer.address() != Address::default()) } /// Attempt to seal generate a proposal seal. @@ -427,7 +430,7 @@ impl Engine for Tendermint { let header = block.header(); let author = header.author(); // Only proposer can generate seal if None was generated. - if !self.is_signer_proposer() || self.proposal.read().is_some() { + if !self.is_signer_proposer(header.parent_hash()) || self.proposal.read().is_some() { return Seal::None; } @@ -441,6 +444,7 @@ impl Engine for Tendermint { self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author); // Remember proposal for later seal submission. *self.proposal.write() = bh; + *self.proposal_parent.write() = header.parent_hash().clone(); Seal::Proposal(vec![ ::rlp::encode(&view).to_vec(), ::rlp::encode(&signature).to_vec(), @@ -505,7 +509,12 @@ impl Engine for Tendermint { } - fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { + fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { + Ok(()) + } + + /// Verify validators and gas limit. + fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> { let proposal = ConsensusMessage::new_proposal(header)?; let proposer = proposal.verify()?; if !self.is_authority(&proposer) { @@ -522,7 +531,7 @@ impl Engine for Tendermint { Some(a) => a, None => public_to_address(&recover(&precommit.signature.into(), &precommit_hash)?), }; - if !self.validators.contains(&address) { + if !self.validators.contains(header.parent_hash(), &address) { Err(EngineError::NotAuthorized(address.to_owned()))? } @@ -545,12 +554,9 @@ impl Engine for Tendermint { found: signatures_len }))?; } - self.is_view_proposer(proposal.vote_step.height, proposal.vote_step.view, &proposer)?; + self.is_view_proposer(header.parent_hash(), proposal.vote_step.height, proposal.vote_step.view, &proposer)?; } - Ok(()) - } - fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> { if header.number() == 0 { Err(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))?; } @@ -595,6 +601,7 @@ impl Engine for Tendermint { debug!(target: "engine", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer); if self.is_view(&proposal) { *self.proposal.write() = proposal.block_hash.clone(); + *self.proposal_parent.write() = header.parent_hash().clone(); } self.votes.vote(proposal, &proposer); true @@ -607,7 +614,7 @@ impl Engine for Tendermint { trace!(target: "engine", "Propose timeout."); if self.proposal.read().is_none() { // Report the proposer if no proposal was received. - let current_proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst)); + let current_proposer = self.view_proposer(&*self.proposal_parent.read(), self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst)); self.validators.report_benign(¤t_proposer); } Step::Prevote @@ -765,20 +772,25 @@ mod tests { let (spec, tap) = setup(); let engine = spec.engine; - let mut header = Header::default(); - let validator = insert_and_unlock(&tap, "0"); - header.set_author(validator); - let seal = proposal_seal(&tap, &header, 0); - header.set_seal(seal); - // Good proposer. - assert!(engine.verify_block_unordered(&header.clone(), None).is_ok()); + let mut parent_header: Header = Header::default(); + parent_header.set_gas_limit(U256::from_str("222222").unwrap()); + let mut header = Header::default(); + header.set_number(1); + header.set_gas_limit(U256::from_str("222222").unwrap()); let validator = insert_and_unlock(&tap, "1"); header.set_author(validator); let seal = proposal_seal(&tap, &header, 0); header.set_seal(seal); + // Good proposer. + assert!(engine.verify_block_family(&header, &parent_header, None).is_ok()); + + let validator = insert_and_unlock(&tap, "0"); + header.set_author(validator); + let seal = proposal_seal(&tap, &header, 0); + header.set_seal(seal); // Bad proposer. - match engine.verify_block_unordered(&header, None) { + match engine.verify_block_family(&header, &parent_header, None) { Err(Error::Engine(EngineError::NotProposer(_))) => {}, _ => panic!(), } @@ -788,7 +800,7 @@ mod tests { let seal = proposal_seal(&tap, &header, 0); header.set_seal(seal); // Not authority. - match engine.verify_block_unordered(&header, None) { + match engine.verify_block_family(&header, &parent_header, None) { Err(Error::Engine(EngineError::NotAuthorized(_))) => {}, _ => panic!(), }; @@ -800,19 +812,24 @@ mod tests { let (spec, tap) = setup(); let engine = spec.engine; + let mut parent_header: Header = Header::default(); + parent_header.set_gas_limit(U256::from_str("222222").unwrap()); + let mut header = Header::default(); + header.set_number(2); + header.set_gas_limit(U256::from_str("222222").unwrap()); let proposer = insert_and_unlock(&tap, "1"); header.set_author(proposer); let mut seal = proposal_seal(&tap, &header, 0); - let vote_info = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash())); + let vote_info = message_info_rlp(&VoteStep::new(2, 0, Step::Precommit), Some(header.bare_hash())); let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap(); seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec(); header.set_seal(seal.clone()); // One good signature is not enough. - match engine.verify_block_unordered(&header, None) { + match engine.verify_block_family(&header, &parent_header, None) { Err(Error::Engine(EngineError::BadSealFieldSize(_))) => {}, _ => panic!(), } @@ -823,7 +840,7 @@ mod tests { seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone()), H520::from(signature0.clone())]).to_vec(); header.set_seal(seal.clone()); - assert!(engine.verify_block_unordered(&header, None).is_ok()); + assert!(engine.verify_block_family(&header, &parent_header, None).is_ok()); let bad_voter = insert_and_unlock(&tap, "101"); let bad_signature = tap.sign(bad_voter, None, vote_info.sha3()).unwrap(); @@ -832,7 +849,7 @@ mod tests { header.set_seal(seal); // One good and one bad signature. - match engine.verify_block_unordered(&header, None) { + match engine.verify_block_family(&header, &parent_header, None) { Err(Error::Engine(EngineError::NotAuthorized(_))) => {}, _ => panic!(), }; diff --git a/ethcore/src/engines/validator_set/contract.rs b/ethcore/src/engines/validator_set/contract.rs index 81bd6b089..91fbf5fab 100644 --- a/ethcore/src/engines/validator_set/contract.rs +++ b/ethcore/src/engines/validator_set/contract.rs @@ -26,30 +26,30 @@ use super::safe_contract::ValidatorSafeContract; /// The validator contract should have the following interface: /// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}] pub struct ValidatorContract { - validators: Arc<ValidatorSafeContract>, + validators: ValidatorSafeContract, provider: RwLock<Option<provider::Contract>>, } impl ValidatorContract { pub fn new(contract_address: Address) -> Self { ValidatorContract { - validators: Arc::new(ValidatorSafeContract::new(contract_address)), + validators: ValidatorSafeContract::new(contract_address), provider: RwLock::new(None), } } } -impl ValidatorSet for Arc<ValidatorContract> { - fn contains(&self, address: &Address) -> bool { - self.validators.contains(address) +impl ValidatorSet for ValidatorContract { + fn contains(&self, bh: &H256, address: &Address) -> bool { + self.validators.contains(bh, address) } - fn get(&self, nonce: usize) -> Address { - self.validators.get(nonce) + fn get(&self, bh: &H256, nonce: usize) -> Address { + self.validators.get(bh, nonce) } - fn count(&self) -> usize { - self.validators.count() + fn count(&self, bh: &H256) -> usize { + self.validators.count(bh) } fn report_malicious(&self, address: &Address) { @@ -144,6 +144,7 @@ mod tests { use header::Header; use account_provider::AccountProvider; use miner::MinerService; + use types::ids::BlockId; use client::BlockChainClient; use tests::helpers::generate_dummy_client_with_spec_and_accounts; use super::super::ValidatorSet; @@ -154,8 +155,9 @@ mod tests { let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_contract, None); let vc = Arc::new(ValidatorContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap())); vc.register_contract(Arc::downgrade(&client)); - assert!(vc.contains(&Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap())); - assert!(vc.contains(&Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap())); + let last_hash = client.best_block_header().hash(); + assert!(vc.contains(&last_hash, &Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap())); + assert!(vc.contains(&last_hash, &Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap())); } #[test] @@ -171,18 +173,21 @@ mod tests { client.miner().set_engine_signer(v1, "".into()).unwrap(); let mut header = Header::default(); - let seal = encode(&vec!(5u8)).to_vec(); - header.set_seal(vec!(seal)); + let seal = vec![encode(&5u8).to_vec(), encode(&(&H520::default() as &[u8])).to_vec()]; + header.set_seal(seal); header.set_author(v1); - header.set_number(1); + header.set_number(2); + header.set_parent_hash(client.chain_info().best_block_hash); + // `reportBenign` when the designated proposer releases block from the future (bad clock). - assert!(client.engine().verify_block_unordered(&header, None).is_err()); + assert!(client.engine().verify_block_family(&header, &header, None).is_err()); // Seal a block. client.engine().step(); assert_eq!(client.chain_info().best_block_number, 1); // Check if the unresponsive validator is `disliked`. - assert_eq!(client.call_contract(validator_contract, "d8f2e0bf".from_hex().unwrap()).unwrap().to_hex(), "0000000000000000000000007d577a597b2742b498cb5cf0c26cdcd726d39e6e"); + assert_eq!(client.call_contract(BlockId::Latest, validator_contract, "d8f2e0bf".from_hex().unwrap()).unwrap().to_hex(), "0000000000000000000000007d577a597b2742b498cb5cf0c26cdcd726d39e6e"); // Simulate a misbehaving validator by handling a double proposal. + let header = client.best_block_header().decode(); assert!(client.engine().verify_block_family(&header, &header, None).is_err()); // Seal a block. client.engine().step(); diff --git a/ethcore/src/engines/validator_set/mod.rs b/ethcore/src/engines/validator_set/mod.rs index 43a6f71d1..3e86c357f 100644 --- a/ethcore/src/engines/validator_set/mod.rs +++ b/ethcore/src/engines/validator_set/mod.rs @@ -21,7 +21,7 @@ mod safe_contract; mod contract; use std::sync::Weak; -use util::{Address, Arc}; +use util::{Address, H256}; use ethjson::spec::ValidatorSet as ValidatorSpec; use client::Client; use self::simple_list::SimpleList; @@ -32,18 +32,18 @@ use self::safe_contract::ValidatorSafeContract; pub fn new_validator_set(spec: ValidatorSpec) -> Box<ValidatorSet + Send + Sync> { match spec { ValidatorSpec::List(list) => Box::new(SimpleList::new(list.into_iter().map(Into::into).collect())), - ValidatorSpec::SafeContract(address) => Box::new(Arc::new(ValidatorSafeContract::new(address.into()))), - ValidatorSpec::Contract(address) => Box::new(Arc::new(ValidatorContract::new(address.into()))), + ValidatorSpec::SafeContract(address) => Box::new(ValidatorSafeContract::new(address.into())), + ValidatorSpec::Contract(address) => Box::new(ValidatorContract::new(address.into())), } } pub trait ValidatorSet { /// Checks if a given address is a validator. - fn contains(&self, address: &Address) -> bool; + fn contains(&self, bh: &H256, address: &Address) -> bool; /// Draws an validator nonce modulo number of validators. - fn get(&self, nonce: usize) -> Address; + fn get(&self, bh: &H256, nonce: usize) -> Address; /// Returns the current number of validators. - fn count(&self) -> usize; + fn count(&self, bh: &H256) -> usize; /// Notifies about malicious behaviour. fn report_malicious(&self, _validator: &Address) {} /// Notifies about benign misbehaviour. diff --git a/ethcore/src/engines/validator_set/safe_contract.rs b/ethcore/src/engines/validator_set/safe_contract.rs index 1a068d858..0a0eaecfd 100644 --- a/ethcore/src/engines/validator_set/safe_contract.rs +++ b/ethcore/src/engines/validator_set/safe_contract.rs @@ -17,17 +17,23 @@ /// Validator set maintained in a contract, updated using `getValidators` method. use std::sync::Weak; +use ethabi; use util::*; +use util::cache::MemoryLruCache; +use types::ids::BlockId; use client::{Client, BlockChainClient}; -use client::chain_notify::ChainNotify; use super::ValidatorSet; use super::simple_list::SimpleList; +const MEMOIZE_CAPACITY: usize = 500; +const CONTRACT_INTERFACE: &'static [u8] = b"[{\"constant\":true,\"inputs\":[],\"name\":\"getValidators\",\"outputs\":[{\"name\":\"\",\"type\":\"address[]\"}],\"payable\":false,\"type\":\"function\"}]"; +const GET_VALIDATORS: &'static str = "getValidators"; + /// The validator contract should have the following interface: /// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}] pub struct ValidatorSafeContract { pub address: Address, - validators: RwLock<SimpleList>, + validators: RwLock<MemoryLruCache<H256, SimpleList>>, provider: RwLock<Option<provider::Contract>>, } @@ -35,102 +41,127 @@ impl ValidatorSafeContract { pub fn new(contract_address: Address) -> Self { ValidatorSafeContract { address: contract_address, - validators: Default::default(), + validators: RwLock::new(MemoryLruCache::new(MEMOIZE_CAPACITY)), provider: RwLock::new(None), } } - /// Queries the state and updates the set of validators. - pub fn update(&self) { + /// Queries the state and gets the set of validators. + fn get_list(&self, block_hash: H256) -> Option<SimpleList> { if let Some(ref provider) = *self.provider.read() { - match provider.get_validators() { + match provider.get_validators(BlockId::Hash(block_hash)) { Ok(new) => { debug!(target: "engine", "Set of validators obtained: {:?}", new); - *self.validators.write() = SimpleList::new(new); + Some(SimpleList::new(new)) + }, + Err(s) => { + debug!(target: "engine", "Set of validators could not be updated: {}", s); + None }, - Err(s) => warn!(target: "engine", "Set of validators could not be updated: {}", s), } } else { - warn!(target: "engine", "Set of validators could not be updated: no provider contract.") + warn!(target: "engine", "Set of validators could not be updated: no provider contract."); + None } } } -/// Checks validators on every block. -impl ChainNotify for ValidatorSafeContract { - fn new_blocks( - &self, - _: Vec<H256>, - _: Vec<H256>, - enacted: Vec<H256>, - _: Vec<H256>, - _: Vec<H256>, - _: Vec<Bytes>, - _duration: u64) { - if !enacted.is_empty() { - self.update(); - } - } -} - -impl ValidatorSet for Arc<ValidatorSafeContract> { - fn contains(&self, address: &Address) -> bool { - self.validators.read().contains(address) +impl ValidatorSet for ValidatorSafeContract { + fn contains(&self, block_hash: &H256, address: &Address) -> bool { + let mut guard = self.validators.write(); + let maybe_existing = guard + .get_mut(block_hash) + .map(|list| list.contains(block_hash, address)); + maybe_existing + .unwrap_or_else(|| self + .get_list(block_hash.clone()) + .map_or(false, |list| { + let contains = list.contains(block_hash, address); + guard.insert(block_hash.clone(), list); + contains + })) } - fn get(&self, nonce: usize) -> Address { - self.validators.read().get(nonce) + fn get(&self, block_hash: &H256, nonce: usize) -> Address { + let mut guard = self.validators.write(); + let maybe_existing = guard + .get_mut(block_hash) + .map(|list| list.get(block_hash, nonce)); + maybe_existing + .unwrap_or_else(|| self + .get_list(block_hash.clone()) + .map_or_else(Default::default, |list| { + let address = list.get(block_hash, nonce); + guard.insert(block_hash.clone(), list); + address + })) } - fn count(&self) -> usize { - self.validators.read().count() + fn count(&self, block_hash: &H256) -> usize { + let mut guard = self.validators.write(); + let maybe_existing = guard + .get_mut(block_hash) + .map(|list| list.count(block_hash)); + maybe_existing + .unwrap_or_else(|| self + .get_list(block_hash.clone()) + .map_or_else(usize::max_value, |list| { + let address = list.count(block_hash); + guard.insert(block_hash.clone(), list); + address + })) } fn register_contract(&self, client: Weak<Client>) { - if let Some(c) = client.upgrade() { - c.add_notify(self.clone()); - } - { - *self.provider.write() = Some(provider::Contract::new(self.address, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d)))); - } - self.update(); + trace!(target: "engine", "Setting up contract caller."); + let contract = ethabi::Contract::new(ethabi::Interface::load(CONTRACT_INTERFACE).expect("JSON interface is valid; qed")); + let call = contract.function(GET_VALIDATORS.into()).expect("Method name is valid; qed"); + let data = call.encode_call(vec![]).expect("get_validators does not take any arguments; qed"); + let contract_address = self.address.clone(); + let do_call = move |id| client + .upgrade() + .ok_or("No client!".into()) + .and_then(|c| c.call_contract(id, contract_address.clone(), data.clone())) + .map(|raw_output| call.decode_output(raw_output).expect("ethabi is correct; qed")); + *self.provider.write() = Some(provider::Contract::new(do_call)); } } mod provider { - // Autogenerated from JSON contract definition using Rust contract convertor. - #![allow(unused_imports)] use std::string::String; use std::result::Result; - use std::fmt; use {util, ethabi}; - use util::{FixedHash, Uint}; + use types::ids::BlockId; pub struct Contract { - contract: ethabi::Contract, - address: util::Address, - do_call: Box<Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static>, + do_call: Box<Fn(BlockId) -> Result<Vec<ethabi::Token>, String> + Send + Sync + 'static>, } + impl Contract { - pub fn new<F>(address: util::Address, do_call: F) -> Self where F: Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static { + pub fn new<F>(do_call: F) -> Self where F: Fn(BlockId) -> Result<Vec<ethabi::Token>, String> + Send + Sync + 'static { Contract { - contract: ethabi::Contract::new(ethabi::Interface::load(b"[{\"constant\":true,\"inputs\":[],\"name\":\"getValidators\",\"outputs\":[{\"name\":\"\",\"type\":\"address[]\"}],\"payable\":false,\"type\":\"function\"}]").expect("JSON is autogenerated; qed")), - address: address, do_call: Box::new(do_call), } } - fn as_string<T: fmt::Debug>(e: T) -> String { format!("{:?}", e) } - - /// Auto-generated from: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}` - #[allow(dead_code)] - pub fn get_validators(&self) -> Result<Vec<util::Address>, String> { - let call = self.contract.function("getValidators".into()).map_err(Self::as_string)?; - let data = call.encode_call( - vec![] - ).map_err(Self::as_string)?; - let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?; - let mut result = output.into_iter().rev().collect::<Vec<_>>(); - Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::<Option<Vec<[u8; 20]>>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::<Vec<_>>() })) + + /// Gets validators from contract with interface: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}` + pub fn get_validators(&self, id: BlockId) -> Result<Vec<util::Address>, String> { + Ok((self.do_call)(id)? + .into_iter() + .rev() + .collect::<Vec<_>>() + .pop() + .expect("get_validators returns one argument; qed") + .to_array() + .and_then(|v| v + .into_iter() + .map(|a| a.to_address()) + .collect::<Option<Vec<[u8; 20]>>>()) + .expect("get_validators returns a list of addresses; qed") + .into_iter() + .map(util::Address::from) + .collect::<Vec<_>>() + ) } } } @@ -138,13 +169,14 @@ mod provider { #[cfg(test)] mod tests { use util::*; + use types::ids::BlockId; use spec::Spec; use account_provider::AccountProvider; use transaction::{Transaction, Action}; use client::{BlockChainClient, EngineClient}; use ethkey::Secret; use miner::MinerService; - use tests::helpers::generate_dummy_client_with_spec_and_accounts; + use tests::helpers::{generate_dummy_client_with_spec_and_accounts, generate_dummy_client_with_spec_and_data}; use super::super::ValidatorSet; use super::ValidatorSafeContract; @@ -153,12 +185,13 @@ mod tests { let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_safe_contract, None); let vc = Arc::new(ValidatorSafeContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap())); vc.register_contract(Arc::downgrade(&client)); - assert!(vc.contains(&Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap())); - assert!(vc.contains(&Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap())); + let last_hash = client.best_block_header().hash(); + assert!(vc.contains(&last_hash, &Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap())); + assert!(vc.contains(&last_hash, &Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap())); } #[test] - fn updates_validators() { + fn knows_validators() { let tap = Arc::new(AccountProvider::transient_provider()); let s0 = Secret::from_slice(&"1".sha3()).unwrap(); let v0 = tap.insert_account(s0.clone(), "").unwrap(); @@ -212,5 +245,14 @@ mod tests { client.update_sealing(); // Able to seal again. assert_eq!(client.chain_info().best_block_number, 3); + + // Check syncing. + let sync_client = generate_dummy_client_with_spec_and_data(Spec::new_validator_safe_contract, 0, 0, &[]); + sync_client.engine().register_client(Arc::downgrade(&sync_client)); + for i in 1..4 { + sync_client.import_block(client.block(BlockId::Number(i)).unwrap().into_inner()).unwrap(); + } + sync_client.flush_queue(); + assert_eq!(sync_client.chain_info().best_block_number, 3); } } diff --git a/ethcore/src/engines/validator_set/simple_list.rs b/ethcore/src/engines/validator_set/simple_list.rs index 3ba574b5a..2d7687979 100644 --- a/ethcore/src/engines/validator_set/simple_list.rs +++ b/ethcore/src/engines/validator_set/simple_list.rs @@ -16,7 +16,7 @@ /// Preconfigured validator list. -use util::Address; +use util::{H256, Address, HeapSizeOf}; use super::ValidatorSet; #[derive(Debug, PartialEq, Eq, Default)] @@ -34,16 +34,22 @@ impl SimpleList { } } +impl HeapSizeOf for SimpleList { + fn heap_size_of_children(&self) -> usize { + self.validators.heap_size_of_children() + self.validator_n.heap_size_of_children() + } +} + impl ValidatorSet for SimpleList { - fn contains(&self, address: &Address) -> bool { + fn contains(&self, _bh: &H256, address: &Address) -> bool { self.validators.contains(address) } - fn get(&self, nonce: usize) -> Address { + fn get(&self, _bh: &H256, nonce: usize) -> Address { self.validators.get(nonce % self.validator_n).expect("There are validator_n authorities; taking number modulo validator_n gives number in validator_n range; qed").clone() } - fn count(&self) -> usize { + fn count(&self, _bh: &H256) -> usize { self.validator_n } } @@ -60,9 +66,9 @@ mod tests { let a1 = Address::from_str("cd1722f3947def4cf144679da39c4c32bdc35681").unwrap(); let a2 = Address::from_str("0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6").unwrap(); let list = SimpleList::new(vec![a1.clone(), a2.clone()]); - assert!(list.contains(&a1)); - assert_eq!(list.get(0), a1); - assert_eq!(list.get(1), a2); - assert_eq!(list.get(2), a1); + assert!(list.contains(&Default::default(), &a1)); + assert_eq!(list.get(&Default::default(), 0), a1); + assert_eq!(list.get(&Default::default(), 1), a2); + assert_eq!(list.get(&Default::default(), 2), a1); } } diff --git a/ethcore/src/miner/service_transaction_checker.rs b/ethcore/src/miner/service_transaction_checker.rs index f3281dade..5a7ab04e9 100644 --- a/ethcore/src/miner/service_transaction_checker.rs +++ b/ethcore/src/miner/service_transaction_checker.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +use types::ids::BlockId; use client::MiningBlockChainClient; use transaction::SignedTransaction; use util::{U256, Uint, Mutex}; @@ -45,7 +46,7 @@ impl ServiceTransactionChecker { debug_assert_eq!(tx.gas_price, U256::zero()); if let Some(ref contract) = *self.contract.lock() { - let do_call = |a, d| client.call_contract(a, d); + let do_call = |a, d| client.call_contract(BlockId::Latest, a, d); contract.certified(&do_call, &tx.sender()) } else { Err("contract is not configured".to_owned()) diff --git a/updater/src/updater.rs b/updater/src/updater.rs index 31c3106bd..605ede5c1 100644 --- a/updater/src/updater.rs +++ b/updater/src/updater.rs @@ -245,7 +245,7 @@ impl Updater { if let Some(ops_addr) = self.client.upgrade().and_then(|c| c.registry_address("operations".into())) { trace!(target: "updater", "Found operations at {}", ops_addr); let client = self.client.clone(); - *self.operations.lock() = Some(Operations::new(ops_addr, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d)))); + *self.operations.lock() = Some(Operations::new(ops_addr, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(BlockId::Latest, a, d)))); } else { // No Operations contract - bail. return; @@ -330,7 +330,7 @@ impl fetch::urlhint::ContractClient for Updater { fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String> { self.client.upgrade().ok_or_else(|| "Client not available".to_owned())? - .call_contract(address, data) + .call_contract(BlockId::Latest, address, data) } } From 731f28a8c021f4b257dffd7b2f19644face88ff7 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Wed, 8 Mar 2017 14:42:56 +0100 Subject: [PATCH 90/93] Better windows icon (#4804) --- windows/ptray/ptray.ico | Bin 67646 -> 116877 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/windows/ptray/ptray.ico b/windows/ptray/ptray.ico index 61e68b90b86ccb69f7c13a0595bba8ffd1943c4c..337475b70f72e51fd991f9b672a275eacd446b0d 100644 GIT binary patch literal 116877 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 literal 67646 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 From a84a10ff03b26fdb9d4706adba2ee4685b4244cb Mon Sep 17 00:00:00 2001 From: keorn <pczaban@gmail.com> Date: Wed, 8 Mar 2017 14:44:07 +0100 Subject: [PATCH 91/93] Add replay protection (#4808) * add eip155 * make network_id default --- ethcore/src/engines/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ethcore/src/engines/mod.rs b/ethcore/src/engines/mod.rs index 2320f49a0..2cc1ff21f 100644 --- a/ethcore/src/engines/mod.rs +++ b/ethcore/src/engines/mod.rs @@ -166,7 +166,9 @@ pub trait Engine : Sync + Send { } /// The network ID that transactions should be signed with. - fn signing_network_id(&self, _env_info: &EnvInfo) -> Option<u64> { None } + fn signing_network_id(&self, _env_info: &EnvInfo) -> Option<u64> { + Some(self.params().chain_id) + } /// Verify the seal of a block. This is an auxilliary method that actually just calls other `verify_` methods /// to get the job done. By default it must pass `verify_basic` and `verify_block_unordered`. If more or fewer From be21671c1c01af248460ff442076af832d6d6c9e Mon Sep 17 00:00:00 2001 From: keorn <pczaban@gmail.com> Date: Wed, 8 Mar 2017 17:25:50 +0100 Subject: [PATCH 92/93] Calibrate step before rejection (#4800) * calibrate before rejection * change flag name * fix merge --- ethcore/src/engines/authority_round.rs | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 45be4a491..03b5d785f 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -83,6 +83,8 @@ pub struct AuthorityRound { client: RwLock<Option<Weak<EngineClient>>>, signer: EngineSigner, validators: Box<ValidatorSet + Send + Sync>, + /// Is this Engine just for testing (prevents step calibration). + calibrate_step: bool, } fn header_step(header: &Header) -> Result<usize, ::rlp::DecoderError> { @@ -122,6 +124,7 @@ impl AuthorityRound { client: RwLock::new(None), signer: Default::default(), validators: new_validator_set(our_params.validators), + calibrate_step: our_params.start_step.is_none(), }); // Do not initialize timeouts for tests. if should_timeout { @@ -131,6 +134,12 @@ impl AuthorityRound { Ok(engine) } + fn calibrate_step(&self) { + if self.calibrate_step { + self.step.store((unix_now().as_secs() / self.step_duration.as_secs()) as usize, AtomicOrdering::SeqCst); + } + } + fn remaining_step_duration(&self) -> Duration { let now = unix_now(); let step_end = self.step_duration * (self.step.load(AtomicOrdering::SeqCst) as u32 + 1); @@ -148,6 +157,16 @@ impl AuthorityRound { fn is_step_proposer(&self, bh: &H256, step: usize, address: &Address) -> bool { self.step_proposer(bh, step) == *address } + + fn is_future_step(&self, step: usize) -> bool { + if step > self.step.load(AtomicOrdering::SeqCst) + 1 { + // Make absolutely sure that the step is correct. + self.calibrate_step(); + step > self.step.load(AtomicOrdering::SeqCst) + 1 + } else { + false + } + } } fn unix_now() -> Duration { @@ -289,18 +308,17 @@ impl Engine for AuthorityRound { fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> { let step = header_step(header)?; // Give one step slack if step is lagging, double vote is still not possible. - if step <= self.step.load(AtomicOrdering::SeqCst) + 1 { - // Check if the signature belongs to a validator, can depend on parent state. + if self.is_future_step(step) { + trace!(target: "engine", "verify_block_unordered: block from the future"); + self.validators.report_benign(header.author()); + Err(BlockError::InvalidSeal)? + } else { let proposer_signature = header_signature(header)?; let correct_proposer = self.step_proposer(header.parent_hash(), step); if !verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? { trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", step); Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))? } - } else { - trace!(target: "engine", "verify_block_unordered: block from the future"); - self.validators.report_benign(header.author()); - Err(BlockError::InvalidSeal)? } // Do not calculate difficulty for genesis blocks. From ca1efc3d776e071885e6944b1b3e542b65f92bd9 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Wed, 8 Mar 2017 18:07:14 +0100 Subject: [PATCH 93/93] Show token icons on list summary pages (#4826) * Adjust balance overlay margins (no jumps) * Img only balances, small verifications * Invalid tests removed * Always wrap display (Thanks @ngotchac) * Update tests to reflect reality --- js/src/ui/AccountCard/accountCard.js | 1 - js/src/ui/AccountCard/accountCard.spec.js | 3 +- js/src/ui/Balance/balance.css | 50 +++++++++++++--------- js/src/ui/Balance/balance.js | 51 ++++++++++++++++++----- js/src/ui/Balance/balance.spec.js | 21 +--------- js/src/views/Accounts/Summary/summary.js | 3 +- js/src/views/Accounts/accounts.css | 23 +++++----- 7 files changed, 87 insertions(+), 65 deletions(-) diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index b5746bf82..107fc39d2 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -87,7 +87,6 @@ export default class AccountCard extends Component { balance={ balance } className={ styles.balance } showOnlyEth - showZeroValues /> </div> diff --git a/js/src/ui/AccountCard/accountCard.spec.js b/js/src/ui/AccountCard/accountCard.spec.js index cecce7a89..6d7e921fd 100644 --- a/js/src/ui/AccountCard/accountCard.spec.js +++ b/js/src/ui/AccountCard/accountCard.spec.js @@ -74,9 +74,8 @@ describe('ui/AccountCard', () => { expect(balance.length).to.equal(1); }); - it('sets showOnlyEth & showZeroValues', () => { + it('sets showOnlyEth', () => { expect(balance.props().showOnlyEth).to.be.true; - expect(balance.props().showZeroValues).to.be.true; }); }); diff --git a/js/src/ui/Balance/balance.css b/js/src/ui/Balance/balance.css index 1d0c9fbf3..1176cafea 100644 --- a/js/src/ui/Balance/balance.css +++ b/js/src/ui/Balance/balance.css @@ -20,11 +20,16 @@ flex-wrap: wrap; margin: 0.75em 0 0 0; vertical-align: top; + + &:not(.full) { + height: 2.5em; + overflow: hidden; + } } .balance, .empty { - margin: 0.75em 0.5em 0 0; + margin: 0.75em 0.5em 4px 0; } .empty { @@ -37,28 +42,35 @@ } .balance { - background: rgba(255, 255, 255, 0.07); + align-items: center; border-radius: 16px; + display: flex; max-height: 24px; max-width: 100%; - display: flex; - align-items: center; -} -.balance img { - height: 32px; - margin: -4px 1em 0 0; - width: 32px; -} + &.full { + background: rgba(255, 255, 255, 0.07); -.balanceValue { - margin: 0 0.5em 0 0; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} + img { + margin-right: 1em; + } + } -.balanceTag { - font-size: 0.85em; - padding-right: 0.75rem; + img { + height: 32px; + margin-top: -4px; + width: 32px; + } + + .tag { + padding-right: 0.75rem; + font-size: 0.85em; + } + + .value { + margin: 0 0.5em 0 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js index 18652fa85..d00e1f01c 100644 --- a/js/src/ui/Balance/balance.js +++ b/js/src/ui/Balance/balance.js @@ -41,7 +41,7 @@ export default class Balance extends Component { render () { const { api } = this.context; - const { balance, className, showZeroValues, showOnlyEth } = this.props; + const { balance, className, showOnlyEth } = this.props; if (!balance || !balance.tokens) { return null; @@ -49,12 +49,13 @@ export default class Balance extends Component { let body = balance.tokens .filter((balance) => { - const hasBalance = showZeroValues || new BigNumber(balance.value).gt(0); - const isValidToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth'; + const isEthToken = (balance.token.tag || '').toLowerCase() === 'eth'; + const hasBalance = new BigNumber(balance.value).gt(0); - return hasBalance && isValidToken; + return hasBalance || isEthToken; }) .map((balance, index) => { + const isFullToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth'; const token = balance.token; let value; @@ -77,16 +78,36 @@ export default class Balance extends Component { value = api.util.fromWei(balance.value).toFormat(3); } + const classNames = [styles.balance]; + let details = null; + + if (isFullToken) { + classNames.push(styles.full); + details = [ + <div + className={ styles.value } + key='value' + > + <span title={ value }> + { value } + </span> + </div>, + <div + className={ styles.tag } + key='tag' + > + { token.tag } + </div> + ]; + } + return ( <div - className={ styles.balance } + className={ classNames.join(' ') } key={ `${index}_${token.tag}` } > <TokenImage token={ token } /> - <div className={ styles.balanceValue }> - <span title={ value }> { value } </span> - </div> - <div className={ styles.balanceTag }> { token.tag } </div> + { details } </div> ); }); @@ -103,7 +124,17 @@ export default class Balance extends Component { } return ( - <div className={ [styles.balances, className].join(' ') }> + <div + className={ + [ + styles.balances, + showOnlyEth + ? '' + : styles.full, + className + ].join(' ') + } + > { body } </div> ); diff --git a/js/src/ui/Balance/balance.spec.js b/js/src/ui/Balance/balance.spec.js index 8a886b39b..ffc25243a 100644 --- a/js/src/ui/Balance/balance.spec.js +++ b/js/src/ui/Balance/balance.spec.js @@ -79,28 +79,9 @@ describe('ui/Balance', () => { }); describe('render specifiers', () => { - it('renders only the single token with showOnlyEth', () => { - render({ showOnlyEth: true }); - expect(component.find('Connect(TokenImage)')).to.have.length(1); - }); - it('renders all the tokens with showZeroValues', () => { render({ showZeroValues: true }); - expect(component.find('Connect(TokenImage)')).to.have.length(3); - }); - - it('shows ETH with zero value with showOnlyEth & showZeroValues', () => { - render({ - showOnlyEth: true, - showZeroValues: true, - balance: { - tokens: [ - { value: '0', token: { tag: 'ETH' } }, - { value: '345', token: { tag: 'GAV', format: 1 } } - ] - } - }); - expect(component.find('Connect(TokenImage)')).to.have.length(1); + expect(component.find('Connect(TokenImage)')).to.have.length(2); }); }); }); diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 78f4dd5c4..601f9d8c2 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -116,7 +116,6 @@ class Summary extends Component { { this.renderBalance(false) } { this.renderDescription(account.meta) } { this.renderOwners() } - { this.renderCertifications() } { this.renderVault(account.meta) } </div> } @@ -155,8 +154,8 @@ class Summary extends Component { </div> <div className={ styles.summary }> { this.renderBalance(true) } - { this.renderCertifications(true) } </div> + { this.renderCertifications(true) } </Container> ); } diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index 296ae6714..de7240cb9 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -41,19 +41,24 @@ margin-top: 1.5em; } + .iconCertifications { + top: 72px; + opacity: 1; + position: absolute; + left: 88px; + + img { + height: 1em !important; + width: 1em !important; + } + } + .summary { position: relative; .ethBalances { opacity: 1; } - - .iconCertifications { - bottom: -0.25em; - opacity: 1; - position: absolute; - right: 0; - } } .overlay { @@ -82,10 +87,6 @@ .ethBalances { opacity: 0; } - - .iconCertifications { - opacity: 0; - } } }