Merge branch 'master' into lightserv
This commit is contained in:
		
						commit
						503b126c93
					
				| @ -108,9 +108,9 @@ linux-centos: | ||||
|     paths: | ||||
|     - target/release/parity | ||||
|     name: "x86_64-unknown-centos-gnu_parity" | ||||
| linux-i686:    | ||||
|   stage: build    | ||||
|   image: ethcore/rust-i686:latest    | ||||
| linux-i686: | ||||
|   stage: build | ||||
|   image: ethcore/rust-i686:latest | ||||
|   only: | ||||
|     - beta | ||||
|     - tags | ||||
| @ -348,7 +348,7 @@ windows: | ||||
|     - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt | ||||
|     - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 | ||||
|     - set RUST_BACKTRACE=1 | ||||
|     - set RUSTFLAGS=%RUSTFLAGS%  | ||||
|     - set RUSTFLAGS=%RUSTFLAGS% | ||||
|     - rustup default stable-x86_64-pc-windows-msvc | ||||
|     - cargo build -j 8 --release #%CARGOFLAGS% | ||||
|     - curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll | ||||
| @ -422,13 +422,14 @@ test-rust-stable: | ||||
|   image: ethcore/rust:stable | ||||
|   before_script: | ||||
|     - git submodule update --init --recursive | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0  ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi | ||||
|     - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v ^js/ | wc -l) | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l) | ||||
|     - echo "rust/js modified: $RUST_FILES_MODIFIED / $JS_FILES_MODIFIED" | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi | ||||
|   script: | ||||
|     - export RUST_BACKTRACE=1 | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"&./test.sh $CARGOFLAGS; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi | ||||
|     - if [ "$RUST_FILES_MODIFIED" = 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi | ||||
|   tags: | ||||
|     - rust | ||||
|     - rust-stable | ||||
| @ -437,13 +438,13 @@ js-test: | ||||
|   image: ethcore/rust:stable | ||||
|   before_script: | ||||
|     - git submodule update --init --recursive | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l) | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi | ||||
|   script: | ||||
|     - export RUST_BACKTRACE=1 | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi | ||||
|   tags: | ||||
|     - rust | ||||
|     - rust-stable | ||||
| @ -484,11 +485,11 @@ js-release: | ||||
|     - stable | ||||
|   image: ethcore/rust:stable | ||||
|   before_script: | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) | ||||
|     - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l) | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0  ]; then echo "skip js build"; else ./js/scripts/install-deps.sh;fi | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0  ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi | ||||
|   script: | ||||
|     - echo $JS_FILES_MODIFIED | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/build.sh&&./js/scripts/release.sh; fi | ||||
|     - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi | ||||
|   tags: | ||||
|     - javascript | ||||
|  | ||||
							
								
								
									
										4
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -676,6 +676,7 @@ 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.5.0", | ||||
|  "ethcore-devtools 1.5.0", | ||||
|  "ethcore-io 1.5.0", | ||||
|  "ethcore-ipc 1.5.0", | ||||
|  "ethcore-ipc-codegen 1.5.0", | ||||
| @ -683,6 +684,7 @@ dependencies = [ | ||||
|  "ethcore-light 1.5.0", | ||||
|  "ethcore-network 1.5.0", | ||||
|  "ethcore-util 1.5.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)", | ||||
|  "parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| @ -1288,7 +1290,7 @@ dependencies = [ | ||||
| [[package]] | ||||
| name = "parity-ui-precompiled" | ||||
| version = "1.4.0" | ||||
| source = "git+https://github.com/ethcore/js-precompiled.git#f5365b857b006ed60c02eb9360d60d7ddef65104" | ||||
| source = "git+https://github.com/ethcore/js-precompiled.git#3d3b2f9e8e8b0fd62c172240bfd001a317cf2979" | ||||
| dependencies = [ | ||||
|  "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
|  | ||||
| @ -4,7 +4,8 @@ | ||||
| 		"AuthorityRound": { | ||||
| 			"params": { | ||||
| 				"gasLimitBoundDivisor": "0x0400", | ||||
| 				"stepDuration": "1", | ||||
| 				"stepDuration": 1, | ||||
| 				"startStep": 2, | ||||
| 				"authorities" : [ | ||||
| 					"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e", | ||||
| 					"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1" | ||||
|  | ||||
| @ -255,6 +255,11 @@ impl Client { | ||||
| 		self.notify.write().push(Arc::downgrade(&target)); | ||||
| 	} | ||||
| 
 | ||||
| 	/// Returns engine reference.
 | ||||
| 	pub fn engine(&self) -> &Engine { | ||||
| 		&*self.engine | ||||
| 	} | ||||
| 
 | ||||
| 	fn notify<F>(&self, f: F) where F: Fn(&ChainNotify) { | ||||
| 		for np in self.notify.read().iter() { | ||||
| 			if let Some(n) = np.upgrade() { | ||||
| @ -563,6 +568,11 @@ impl Client { | ||||
| 		results.len() | ||||
| 	} | ||||
| 
 | ||||
| 	/// Get shared miner reference.
 | ||||
| 	pub fn miner(&self) -> Arc<Miner> { | ||||
| 		self.miner.clone() | ||||
| 	} | ||||
| 
 | ||||
| 	/// Used by PoA to try sealing on period change.
 | ||||
| 	pub fn update_sealing(&self) { | ||||
| 		self.miner.update_sealing(self) | ||||
| @ -1060,6 +1070,10 @@ impl BlockChainClient for Client { | ||||
| 		self.transaction_address(id).and_then(|address| self.chain.read().transaction(&address)) | ||||
| 	} | ||||
| 
 | ||||
| 	fn transaction_block(&self, id: TransactionID) -> Option<H256> { | ||||
| 		self.transaction_address(id).map(|addr| addr.block_hash) | ||||
| 	} | ||||
| 
 | ||||
| 	fn uncle(&self, id: UncleID) -> Option<Bytes> { | ||||
| 		let index = id.position; | ||||
| 		self.block_body(id.block).and_then(|body| BodyView::new(&body).uncle_rlp_at(index)) | ||||
| @ -1433,4 +1447,4 @@ mod tests { | ||||
| 
 | ||||
| 		assert!(client.tree_route(&genesis, &new_hash).is_none()); | ||||
| 	} | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -255,7 +255,7 @@ impl TestBlockChainClient { | ||||
| 	} | ||||
| 
 | ||||
| 	/// Make a bad block by setting invalid extra data.
 | ||||
| 	pub fn corrupt_block(&mut self, n: BlockNumber) { | ||||
| 	pub fn corrupt_block(&self, n: BlockNumber) { | ||||
| 		let hash = self.block_hash(BlockID::Number(n)).unwrap(); | ||||
| 		let mut header: BlockHeader = decode(&self.block_header(BlockID::Number(n)).unwrap()); | ||||
| 		header.set_extra_data(b"This extra data is way too long to be considered valid".to_vec()); | ||||
| @ -267,7 +267,7 @@ impl TestBlockChainClient { | ||||
| 	} | ||||
| 
 | ||||
| 	/// Make a bad block by setting invalid parent hash.
 | ||||
| 	pub fn corrupt_block_parent(&mut self, n: BlockNumber) { | ||||
| 	pub fn corrupt_block_parent(&self, n: BlockNumber) { | ||||
| 		let hash = self.block_hash(BlockID::Number(n)).unwrap(); | ||||
| 		let mut header: BlockHeader = decode(&self.block_header(BlockID::Number(n)).unwrap()); | ||||
| 		header.set_parent_hash(H256::from(42)); | ||||
| @ -432,6 +432,10 @@ impl BlockChainClient for TestBlockChainClient { | ||||
| 		None	// Simple default.
 | ||||
| 	} | ||||
| 
 | ||||
| 	fn transaction_block(&self, _id: TransactionID) -> Option<H256> { | ||||
| 		None	// Simple default.
 | ||||
| 	} | ||||
| 
 | ||||
| 	fn uncle(&self, _id: UncleID) -> Option<Bytes> { | ||||
| 		None	// Simple default.
 | ||||
| 	} | ||||
|  | ||||
| @ -129,6 +129,9 @@ pub trait BlockChainClient : Sync + Send { | ||||
| 	/// Get transaction with given hash.
 | ||||
| 	fn transaction(&self, id: TransactionID) -> Option<LocalizedTransaction>; | ||||
| 
 | ||||
| 	/// Get the hash of block that contains the transaction, if any.
 | ||||
| 	fn transaction_block(&self, id: TransactionID) -> Option<H256>; | ||||
| 
 | ||||
| 	/// Get uncle with given id.
 | ||||
| 	fn uncle(&self, id: UncleID) -> Option<Bytes>; | ||||
| 
 | ||||
|  | ||||
| @ -49,6 +49,8 @@ pub struct AuthorityRoundParams { | ||||
| 	pub authorities: Vec<Address>, | ||||
| 	/// Number of authorities.
 | ||||
| 	pub authority_n: usize, | ||||
| 	/// Starting step,
 | ||||
| 	pub start_step: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| impl From<ethjson::spec::AuthorityRoundParams> for AuthorityRoundParams { | ||||
| @ -58,6 +60,7 @@ impl From<ethjson::spec::AuthorityRoundParams> for AuthorityRoundParams { | ||||
| 			step_duration: Duration::from_secs(p.step_duration.into()), | ||||
| 			authority_n: p.authorities.len(), | ||||
| 			authorities: p.authorities.into_iter().map(Into::into).collect::<Vec<_>>(), | ||||
| 			start_step: p.start_step.map(Into::into), | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -97,7 +100,7 @@ impl AsMillis for Duration { | ||||
| impl AuthorityRound { | ||||
| 	/// Create a new instance of AuthorityRound engine.
 | ||||
| 	pub fn new(params: CommonParams, our_params: AuthorityRoundParams, builtins: BTreeMap<Address, Builtin>) -> Result<Arc<Self>, Error> { | ||||
| 		let initial_step = (unix_now().as_secs() / our_params.step_duration.as_secs()) as usize; | ||||
| 		let initial_step = our_params.start_step.unwrap_or_else(|| (unix_now().as_secs() / our_params.step_duration.as_secs())) as usize; | ||||
| 		let engine = Arc::new( | ||||
| 			AuthorityRound { | ||||
| 				params: params, | ||||
| @ -160,14 +163,7 @@ impl IoHandler<()> for TransitionHandler { | ||||
| 	fn timeout(&self, io: &IoContext<()>, timer: TimerToken) { | ||||
| 		if timer == ENGINE_TIMEOUT_TOKEN { | ||||
| 			if let Some(engine) = self.engine.upgrade() { | ||||
| 				engine.step.fetch_add(1, AtomicOrdering::SeqCst); | ||||
| 				engine.proposed.store(false, AtomicOrdering::SeqCst); | ||||
| 				if let Some(ref channel) = *engine.message_channel.lock() { | ||||
| 					match channel.send(ClientIoMessage::UpdateSealing) { | ||||
| 						Ok(_) => trace!(target: "poa", "timeout: UpdateSealing message sent for step {}.", engine.step.load(AtomicOrdering::Relaxed)), | ||||
| 						Err(err) => trace!(target: "poa", "timeout: Could not send a sealing message {} for step {}.", err, engine.step.load(AtomicOrdering::Relaxed)), | ||||
| 					} | ||||
| 				} | ||||
| 				engine.step(); | ||||
| 				io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis()) | ||||
| 					.unwrap_or_else(|e| warn!(target: "poa", "Failed to restart consensus step timer: {}.", e)) | ||||
| 			} | ||||
| @ -184,6 +180,17 @@ impl Engine for AuthorityRound { | ||||
| 	fn params(&self) -> &CommonParams { &self.params } | ||||
| 	fn builtins(&self) -> &BTreeMap<Address, Builtin> { &self.builtins } | ||||
| 
 | ||||
| 	fn step(&self) { | ||||
| 		self.step.fetch_add(1, AtomicOrdering::SeqCst); | ||||
| 		self.proposed.store(false, AtomicOrdering::SeqCst); | ||||
| 		if let Some(ref channel) = *self.message_channel.lock() { | ||||
| 			match channel.send(ClientIoMessage::UpdateSealing) { | ||||
| 				Ok(_) => trace!(target: "poa", "timeout: UpdateSealing message sent for step {}.", self.step.load(AtomicOrdering::Relaxed)), | ||||
| 				Err(err) => trace!(target: "poa", "timeout: Could not send a sealing message {} for step {}.", err, self.step.load(AtomicOrdering::Relaxed)), | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/// Additional engine-specific information for the user/developer concerning `header`.
 | ||||
| 	fn extra_info(&self, header: &Header) -> BTreeMap<String, String> { | ||||
| 		map![ | ||||
| @ -235,6 +242,8 @@ impl Engine for AuthorityRound { | ||||
| 			} else { | ||||
| 				warn!(target: "poa", "generate_seal: FAIL: Accounts not provided."); | ||||
| 			} | ||||
| 		} else { | ||||
| 			trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step); | ||||
| 		} | ||||
| 		None | ||||
| 	} | ||||
|  | ||||
| @ -160,4 +160,6 @@ pub trait Engine : Sync + Send { | ||||
| 
 | ||||
| 	/// Add an account provider useful for Engines that sign stuff.
 | ||||
| 	fn register_account_provider(&self, _account_provider: Arc<AccountProvider>) {} | ||||
| 	/// Trigger next step of the consensus engine.
 | ||||
| 	fn step(&self) {} | ||||
| } | ||||
|  | ||||
| @ -23,7 +23,7 @@ use account_provider::{AccountProvider, Error as AccountError}; | ||||
| use views::{BlockView, HeaderView}; | ||||
| use header::Header; | ||||
| use state::{State, CleanupMode}; | ||||
| use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics}; | ||||
| use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics, TransactionID}; | ||||
| use client::TransactionImportResult; | ||||
| use executive::contract_address; | ||||
| use block::{ClosedBlock, SealedBlock, IsBlock, Block}; | ||||
| @ -357,6 +357,8 @@ impl Miner { | ||||
| 		let block_number = open_block.block().fields().header.number(); | ||||
| 
 | ||||
| 		// TODO Push new uncles too.
 | ||||
| 		let mut tx_count: usize = 0; | ||||
| 		let tx_total = transactions.len(); | ||||
| 		for tx in transactions { | ||||
| 			let hash = tx.hash(); | ||||
| 			let start = Instant::now(); | ||||
| @ -378,7 +380,7 @@ impl Miner { | ||||
| 				}, | ||||
| 				_ => {}, | ||||
| 			} | ||||
| 
 | ||||
| 			trace!(target: "miner", "Adding tx {:?} took {:?}", hash, took); | ||||
| 			match result { | ||||
| 				Err(Error::Execution(ExecutionError::BlockGasLimitReached { gas_limit, gas_used, gas })) => { | ||||
| 					debug!(target: "miner", "Skipping adding transaction to block because of gas limit: {:?} (limit: {:?}, used: {:?}, gas: {:?})", hash, gas_limit, gas_used, gas); | ||||
| @ -407,9 +409,12 @@ impl Miner { | ||||
| 						   "Error adding transaction to block: number={}. transaction_hash={:?}, Error: {:?}", | ||||
| 						   block_number, hash, e); | ||||
| 				}, | ||||
| 				_ => {}	// imported ok
 | ||||
| 				_ => { | ||||
| 					tx_count += 1; | ||||
| 				}	// imported ok
 | ||||
| 			} | ||||
| 		} | ||||
| 		trace!(target: "miner", "Pushed {}/{} transactions", tx_count, tx_total); | ||||
| 
 | ||||
| 		let block = open_block.close(); | ||||
| 
 | ||||
| @ -580,6 +585,10 @@ impl Miner { | ||||
| 		let best_block_header: Header = ::rlp::decode(&chain.best_block_header()); | ||||
| 		transactions.into_iter() | ||||
| 			.map(|tx| { | ||||
| 				if chain.transaction_block(TransactionID::Hash(tx.hash())).is_some() { | ||||
| 					debug!(target: "miner", "Rejected tx {:?}: already in the blockchain", tx.hash()); | ||||
| 					return Err(Error::Transaction(TransactionError::AlreadyImported)); | ||||
| 				} | ||||
| 				match self.engine.verify_transaction_basic(&tx, &best_block_header) { | ||||
| 					Err(e) => { | ||||
| 						debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e); | ||||
|  | ||||
| @ -273,7 +273,7 @@ impl Spec { | ||||
| 	pub fn new_instant() -> Spec { load_bundled!("instant_seal") } | ||||
| 
 | ||||
| 	/// Create a new Spec with AuthorityRound consensus which does internal sealing (not requiring work).
 | ||||
| 	/// Accounts with secrets "1".sha3() and "2".sha3() are the authorities.
 | ||||
| 	/// Accounts with secrets "0".sha3() and "1".sha3() are the authorities.
 | ||||
| 	pub fn new_test_round() -> Self { load_bundled!("authority_round") } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "parity.js", | ||||
|   "version": "0.2.100", | ||||
|   "version": "0.2.105", | ||||
|   "main": "release/index.js", | ||||
|   "jsnext:main": "src/index.js", | ||||
|   "author": "Parity Team <admin@parity.io>", | ||||
|  | ||||
							
								
								
									
										8
									
								
								js/src/3rdparty/etherscan/links.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								js/src/3rdparty/etherscan/links.js
									
									
									
									
										vendored
									
									
								
							| @ -14,10 +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/>.
 | ||||
| 
 | ||||
| export const url = (isTestnet = false) => { | ||||
|   return `https://${isTestnet ? 'testnet.' : ''}etherscan.io`; | ||||
| }; | ||||
| 
 | ||||
| export const txLink = (hash, isTestnet = false) => { | ||||
|   return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/tx/${hash}`; | ||||
|   return `${url(isTestnet)}/tx/${hash}`; | ||||
| }; | ||||
| 
 | ||||
| export const addressLink = (address, isTestnet = false) => { | ||||
|   return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/address/${address}`; | ||||
|   return `${url(isTestnet)}/address/${address}`; | ||||
| }; | ||||
|  | ||||
| @ -240,8 +240,8 @@ export default class Contract { | ||||
|   } | ||||
| 
 | ||||
|   _bindEvent = (event) => { | ||||
|     event.subscribe = (options = {}, callback) => { | ||||
|       return this._subscribe(event, options, callback); | ||||
|     event.subscribe = (options = {}, callback, autoRemove) => { | ||||
|       return this._subscribe(event, options, callback, autoRemove); | ||||
|     }; | ||||
| 
 | ||||
|     event.unsubscribe = (subscriptionId) => { | ||||
| @ -306,16 +306,31 @@ export default class Contract { | ||||
|     return this._api.eth.newFilter(options); | ||||
|   } | ||||
| 
 | ||||
|   subscribe (eventName = null, options = {}, callback) { | ||||
|   subscribe (eventName = null, options = {}, callback, autoRemove) { | ||||
|     try { | ||||
|       const event = this._findEvent(eventName); | ||||
|       return this._subscribe(event, options, callback); | ||||
|       return this._subscribe(event, options, callback, autoRemove); | ||||
|     } catch (e) { | ||||
|       return Promise.reject(e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _subscribe (event = null, _options, callback) { | ||||
|   _sendData (subscriptionId, error, logs) { | ||||
|     const { autoRemove, callback } = this._subscriptions[subscriptionId]; | ||||
|     let result = true; | ||||
| 
 | ||||
|     try { | ||||
|       result = callback(error, logs); | ||||
|     } catch (error) { | ||||
|       console.warn('_sendData', subscriptionId, error); | ||||
|     } | ||||
| 
 | ||||
|     if (autoRemove && result && typeof result === 'boolean') { | ||||
|       this.unsubscribe(subscriptionId); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _subscribe (event = null, _options, callback, autoRemove = false) { | ||||
|     const subscriptionId = nextSubscriptionId++; | ||||
|     const { skipInitFetch } = _options; | ||||
|     delete _options['skipInitFetch']; | ||||
| @ -325,6 +340,7 @@ export default class Contract { | ||||
|       .then((filterId) => { | ||||
|         this._subscriptions[subscriptionId] = { | ||||
|           options: _options, | ||||
|           autoRemove, | ||||
|           callback, | ||||
|           filterId | ||||
|         }; | ||||
| @ -337,8 +353,7 @@ export default class Contract { | ||||
|         return this._api.eth | ||||
|           .getFilterLogs(filterId) | ||||
|           .then((logs) => { | ||||
|             callback(null, this.parseEventLogs(logs)); | ||||
| 
 | ||||
|             this._sendData(subscriptionId, null, this.parseEventLogs(logs)); | ||||
|             this._subscribeToChanges(); | ||||
|             return subscriptionId; | ||||
|           }); | ||||
| @ -437,13 +452,13 @@ export default class Contract { | ||||
|         }) | ||||
|       ) | ||||
|       .then((logsArray) => { | ||||
|         logsArray.forEach((logs, idx) => { | ||||
|         logsArray.forEach((logs, subscriptionId) => { | ||||
|           if (!logs || !logs.length) { | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           try { | ||||
|             subscriptions[idx].callback(null, this.parseEventLogs(logs)); | ||||
|             this.sendData(subscriptionId, null, this.parseEventLogs(logs)); | ||||
|           } catch (error) { | ||||
|             console.error('_sendSubscriptionChanges', error); | ||||
|           } | ||||
|  | ||||
| @ -59,7 +59,7 @@ export default class Manager { | ||||
|     return subscription; | ||||
|   } | ||||
| 
 | ||||
|   subscribe (subscriptionName, callback) { | ||||
|   subscribe (subscriptionName, callback, autoRemove = false) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       const subscription = this._validateType(subscriptionName); | ||||
| 
 | ||||
| @ -75,6 +75,7 @@ export default class Manager { | ||||
|       this.subscriptions[subscriptionId] = { | ||||
|         name: subscriptionName, | ||||
|         id: subscriptionId, | ||||
|         autoRemove, | ||||
|         callback | ||||
|       }; | ||||
| 
 | ||||
| @ -101,13 +102,18 @@ export default class Manager { | ||||
|   } | ||||
| 
 | ||||
|   _sendData (subscriptionId, error, data) { | ||||
|     const { callback } = this.subscriptions[subscriptionId]; | ||||
|     const { autoRemove, callback } = this.subscriptions[subscriptionId]; | ||||
|     let result = true; | ||||
| 
 | ||||
|     try { | ||||
|       callback(error, data); | ||||
|       result = callback(error, data); | ||||
|     } catch (error) { | ||||
|       console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error); | ||||
|     } | ||||
| 
 | ||||
|     if (autoRemove && result && typeof result === 'boolean') { | ||||
|       this.unsubscribe(subscriptionId); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _updateSubscriptions = (subscriptionName, error, data) => { | ||||
|  | ||||
| @ -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 wallet from './wallet'; | ||||
| import { wallet } from './wallet'; | ||||
| 
 | ||||
| export { | ||||
|   wallet | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										460
									
								
								js/src/contracts/snippets/enhanced-wallet.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								js/src/contracts/snippets/enhanced-wallet.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,460 @@ | ||||
| //sol Wallet | ||||
| // Multi-sig, daily-limited account proxy/wallet. | ||||
| // @authors: | ||||
| // Gav Wood <g@ethdev.com> | ||||
| // inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a | ||||
| // single, or, crucially, each of a number of, designated owners. | ||||
| // usage: | ||||
| // 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 | ||||
| 
 | ||||
|     // 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); | ||||
| 
 | ||||
|     // 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); | ||||
| 
 | ||||
|     // 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); | ||||
| } | ||||
| 
 | ||||
| contract multisigAbi is multisig { | ||||
|     function isOwner(address _addr) returns (bool); | ||||
| 
 | ||||
|     function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool); | ||||
| 
 | ||||
|     function confirm(bytes32 _h) returns(bool); | ||||
| 
 | ||||
|     // (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 addOwner(address _owner); | ||||
| 
 | ||||
|     function removeOwner(address _owner); | ||||
| 
 | ||||
|     function changeRequirement(uint _newRequired); | ||||
| 
 | ||||
|     // Revokes a prior confirmation of the given operation | ||||
|     function revoke(bytes32 _operation); | ||||
| 
 | ||||
|     function changeOwner(address _from, address _to); | ||||
| 
 | ||||
|     function execute(address _to, uint _value, bytes _data) returns(bool); | ||||
| } | ||||
| 
 | ||||
| contract WalletLibrary is multisig { | ||||
|     // TYPES | ||||
| 
 | ||||
|     // 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; | ||||
|     } | ||||
| 
 | ||||
|     /****************************** | ||||
|      ***** MULTI OWNED SECTION **** | ||||
|      ******************************/ | ||||
| 
 | ||||
|     // 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 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; | ||||
| 
 | ||||
|         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; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|         // 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) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // 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 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 | ||||
| 
 | ||||
|     function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { | ||||
|         return _walletLibrary.delegatecall(msg.data); | ||||
|     } | ||||
| 
 | ||||
|     function isOwner(address _addr) returns (bool) { | ||||
|         return _walletLibrary.delegatecall(msg.data); | ||||
|     } | ||||
| 
 | ||||
|     // 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; | ||||
| 
 | ||||
|     uint public m_dailyLimit; | ||||
|     uint public m_spentToday; | ||||
|     uint public m_lastDay; | ||||
| 
 | ||||
|     // list of owners | ||||
|     uint[256] m_owners; | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| import babel from 'rollup-plugin-babel'; | ||||
| 
 | ||||
| export default { | ||||
|   entry: 'src/index.js', | ||||
|   dest: 'release/index.js', | ||||
|   format: 'cjs', | ||||
|   plugins: [babel({ | ||||
|     babelrc: false, | ||||
|     presets: ['es2015-rollup', 'stage-0'], | ||||
|     runtimeHelpers: true | ||||
|   })] | ||||
| }; | ||||
| @ -19,7 +19,7 @@ import ContentAdd from 'material-ui/svg-icons/content/add'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| 
 | ||||
| import { Button, Modal, Form, Input, InputAddress } from '~/ui'; | ||||
| import { ERRORS, validateAddress, validateName } from '../../util/validation'; | ||||
| import { ERRORS, validateAddress, validateName } from '~/util/validation'; | ||||
| 
 | ||||
| export default class AddAddress extends Component { | ||||
|   static contextTypes = { | ||||
|  | ||||
| @ -21,7 +21,7 @@ import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forwa | ||||
| import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; | ||||
| 
 | ||||
| import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; | ||||
| import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; | ||||
| import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation'; | ||||
| 
 | ||||
| import { eip20, wallet } from '~/contracts/abi'; | ||||
| 
 | ||||
|  | ||||
| @ -17,6 +17,7 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import { RadioButtons } from '~/ui'; | ||||
| import { walletSourceURL } from '~/contracts/code/wallet'; | ||||
| 
 | ||||
| // import styles from '../createWallet.css';
 | ||||
| 
 | ||||
| @ -46,7 +47,9 @@ export default class WalletType extends Component { | ||||
|         description: ( | ||||
|           <span> | ||||
|             <span>Create/Deploy a </span> | ||||
|             <a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>standard multi-signature </a> | ||||
|             <a href={ walletSourceURL } target='_blank'> | ||||
|               standard multi-signature | ||||
|             </a> | ||||
|             <span> Wallet</span> | ||||
|           </span> | ||||
|         ) | ||||
|  | ||||
| @ -16,13 +16,13 @@ | ||||
| 
 | ||||
| import { observable, computed, action, transaction } from 'mobx'; | ||||
| 
 | ||||
| import { validateUint, validateAddress, validateName } from '~/util/validation'; | ||||
| import { ERROR_CODES } from '~/api/transport/error'; | ||||
| 
 | ||||
| import Contract from '~/api/contract'; | ||||
| import Contracts from '~/contracts'; | ||||
| import { ERROR_CODES } from '~/api/transport/error'; | ||||
| import { wallet as walletAbi } from '~/contracts/abi'; | ||||
| import { wallet as walletCode } from '~/contracts/code'; | ||||
| import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; | ||||
| 
 | ||||
| import { validateUint, validateAddress, validateName } from '~/util/validation'; | ||||
| import WalletsUtils from '~/util/wallets'; | ||||
| 
 | ||||
| const STEPS = { | ||||
| @ -160,14 +160,25 @@ export default class CreateWalletStore { | ||||
| 
 | ||||
|     const { account, owners, required, daylimit } = this.wallet; | ||||
| 
 | ||||
|     const options = { | ||||
|       data: walletCode, | ||||
|       from: account | ||||
|     }; | ||||
|     Contracts | ||||
|       .get() | ||||
|       .registry | ||||
|       .lookupAddress(walletLibraryRegKey) | ||||
|       .then((address) => { | ||||
|         const walletLibraryAddress = (address || '').replace(/^0x/, '').toLowerCase(); | ||||
|         const code = walletLibraryAddress.length && !/^0+$/.test(walletLibraryAddress) | ||||
|           ? walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress) | ||||
|           : fullWalletCode; | ||||
| 
 | ||||
|     this.api | ||||
|       .newContract(walletAbi) | ||||
|       .deploy(options, [ owners, required, daylimit ], this.onDeploymentState) | ||||
|         const options = { | ||||
|           data: code, | ||||
|           from: account | ||||
|         }; | ||||
| 
 | ||||
|         return this.api | ||||
|           .newContract(walletAbi) | ||||
|           .deploy(options, [ owners, required, daylimit ], this.onDeploymentState); | ||||
|       }) | ||||
|       .then((address) => { | ||||
|         this.deployed = true; | ||||
|         this.wallet.address = address; | ||||
|  | ||||
| @ -18,8 +18,8 @@ import React, { Component, PropTypes } from 'react'; | ||||
| import { MenuItem } from 'material-ui'; | ||||
| 
 | ||||
| import { AddressSelect, Form, Input, Select } from '~/ui'; | ||||
| import { validateAbi } from '../../../util/validation'; | ||||
| import { parseAbiType } from '../../../util/abi'; | ||||
| import { validateAbi } from '~/util/validation'; | ||||
| import { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| export default class DetailsStep extends Component { | ||||
|   static contextTypes = { | ||||
|  | ||||
| @ -32,7 +32,7 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import { Form, TypedInput } from '~/ui'; | ||||
| import { parseAbiType } from '../../../util/abi'; | ||||
| import { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| import styles from '../deployContract.css'; | ||||
| 
 | ||||
|  | ||||
| @ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| 
 | ||||
| import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; | ||||
| import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation'; | ||||
| import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; | ||||
| 
 | ||||
| import DetailsStep from './DetailsStep'; | ||||
| import ParametersStep from './ParametersStep'; | ||||
|  | ||||
| @ -19,7 +19,7 @@ import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| import ContentSave from 'material-ui/svg-icons/content/save'; | ||||
| 
 | ||||
| import { Button, Form, Input, InputChip, Modal } from '~/ui'; | ||||
| import { validateName } from '../../util/validation'; | ||||
| import { validateName } from '~/util/validation'; | ||||
| 
 | ||||
| export default class EditMeta extends Component { | ||||
|   static contextTypes = { | ||||
|  | ||||
| @ -15,13 +15,19 @@ | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { MenuItem } from 'material-ui'; | ||||
| import { Checkbox, MenuItem } from 'material-ui'; | ||||
| 
 | ||||
| import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui'; | ||||
| import { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| import styles from '../executeContract.css'; | ||||
| 
 | ||||
| const CHECK_STYLE = { | ||||
|   position: 'absolute', | ||||
|   top: '38px', | ||||
|   left: '1em' | ||||
| }; | ||||
| 
 | ||||
| export default class DetailsStep extends Component { | ||||
|   static propTypes = { | ||||
|     accounts: PropTypes.object.isRequired, | ||||
| @ -31,10 +37,12 @@ export default class DetailsStep extends Component { | ||||
|     onAmountChange: PropTypes.func.isRequired, | ||||
|     fromAddress: PropTypes.string, | ||||
|     fromAddressError: PropTypes.string, | ||||
|     gasEdit: PropTypes.bool, | ||||
|     onFromAddressChange: PropTypes.func.isRequired, | ||||
|     func: PropTypes.object, | ||||
|     funcError: PropTypes.string, | ||||
|     onFuncChange: PropTypes.func, | ||||
|     onGasEditClick: PropTypes.func, | ||||
|     values: PropTypes.array.isRequired, | ||||
|     valuesError: PropTypes.array.isRequired, | ||||
|     warning: PropTypes.string, | ||||
| @ -42,7 +50,7 @@ export default class DetailsStep extends Component { | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { accounts, amount, amountError, fromAddress, fromAddressError, onFromAddressChange, onAmountChange } = this.props; | ||||
|     const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
| @ -56,12 +64,23 @@ export default class DetailsStep extends Component { | ||||
|           onChange={ onFromAddressChange } /> | ||||
|         { this.renderFunctionSelect() } | ||||
|         { this.renderParameters() } | ||||
|         <Input | ||||
|           label='transaction value (in ETH)' | ||||
|           hint='the amount to send to with the transaction' | ||||
|           value={ amount } | ||||
|           error={ amountError } | ||||
|           onSubmit={ onAmountChange } /> | ||||
|         <div className={ styles.columns }> | ||||
|           <div> | ||||
|             <Input | ||||
|               label='transaction value (in ETH)' | ||||
|               hint='the amount to send to with the transaction' | ||||
|               value={ amount } | ||||
|               error={ amountError } | ||||
|               onSubmit={ onAmountChange } /> | ||||
|           </div> | ||||
|           <div> | ||||
|             <Checkbox | ||||
|               checked={ gasEdit } | ||||
|               label='edit gas price or value' | ||||
|               onCheck={ onGasEditClick } | ||||
|               style={ CHECK_STYLE } /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </Form> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -42,3 +42,15 @@ | ||||
|   padding: 0.75em; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .columns { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
| 
 | ||||
|   &>div { | ||||
|     flex: 0 1 50%; | ||||
|     width: 50%; | ||||
|     position: relative; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -17,19 +17,36 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import { observer } from 'mobx-react'; | ||||
| import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; | ||||
| import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; | ||||
| 
 | ||||
| import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui'; | ||||
| import { MAX_GAS_ESTIMATION } from '../../util/constants'; | ||||
| import { validateAddress, validateUint } from '../../util/validation'; | ||||
| import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui'; | ||||
| import { MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| import { validateAddress, validateUint } from '~/util/validation'; | ||||
| import { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| import DetailsStep from './DetailsStep'; | ||||
| 
 | ||||
| import ERRORS from '../Transfer/errors'; | ||||
| import { ERROR_CODES } from '~/api/transport/error'; | ||||
| 
 | ||||
| const STEP_DETAILS = 0; | ||||
| const STEP_BUSY_OR_GAS = 1; | ||||
| const STEP_BUSY = 2; | ||||
| 
 | ||||
| const TITLES = { | ||||
|   transfer: 'function details', | ||||
|   sending: 'sending', | ||||
|   complete: 'complete', | ||||
|   gas: 'gas selection', | ||||
|   rejected: 'rejected' | ||||
| }; | ||||
| const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; | ||||
| const STAGES_GAS = [TITLES.transfer, TITLES.gas, TITLES.sending, TITLES.complete]; | ||||
| 
 | ||||
| @observer | ||||
| class ExecuteContract extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
| @ -46,21 +63,22 @@ class ExecuteContract extends Component { | ||||
|     onFromAddressChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit); | ||||
| 
 | ||||
|   state = { | ||||
|     amount: '0', | ||||
|     amountError: null, | ||||
|     busyState: null, | ||||
|     fromAddressError: null, | ||||
|     func: null, | ||||
|     funcError: null, | ||||
|     gas: null, | ||||
|     gasLimitError: null, | ||||
|     gasEdit: false, | ||||
|     rejected: false, | ||||
|     step: STEP_DETAILS, | ||||
|     sending: false, | ||||
|     values: [], | ||||
|     valuesError: [], | ||||
|     step: 0, | ||||
|     sending: false, | ||||
|     busyState: null, | ||||
|     txhash: null, | ||||
|     rejected: false | ||||
|     txhash: null | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
| @ -79,15 +97,21 @@ class ExecuteContract extends Component { | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { sending } = this.state; | ||||
|     const { sending, step, gasEdit, rejected } = this.state; | ||||
|     const steps = gasEdit ? STAGES_GAS : STAGES_BASIC; | ||||
| 
 | ||||
|     if (rejected) { | ||||
|       steps[steps.length - 1] = TITLES.rejected; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Modal | ||||
|         actions={ this.renderDialogActions() } | ||||
|         title='execute function' | ||||
|         busy={ sending } | ||||
|         waiting={ [1] } | ||||
|         visible> | ||||
|         current={ step } | ||||
|         steps={ steps } | ||||
|         visible | ||||
|         waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }> | ||||
|         { this.renderStep() } | ||||
|       </Modal> | ||||
|     ); | ||||
| @ -95,7 +119,7 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   renderDialogActions () { | ||||
|     const { onClose, fromAddress } = this.props; | ||||
|     const { sending, step, fromAddressError, valuesError } = this.state; | ||||
|     const { gasEdit, sending, step, fromAddressError, valuesError } = this.state; | ||||
|     const hasError = fromAddressError || valuesError.find((error) => error); | ||||
| 
 | ||||
|     const cancelBtn = ( | ||||
| @ -105,21 +129,44 @@ class ExecuteContract extends Component { | ||||
|         icon={ <ContentClear /> } | ||||
|         onClick={ onClose } /> | ||||
|     ); | ||||
|     const postBtn = ( | ||||
|       <Button | ||||
|         key='postTransaction' | ||||
|         label='post transaction' | ||||
|         disabled={ !!(sending || hasError) } | ||||
|         icon={ <IdentityIcon address={ fromAddress } button /> } | ||||
|         onClick={ this.postTransaction } /> | ||||
|     ); | ||||
|     const nextBtn = ( | ||||
|       <Button | ||||
|         key='nextStep' | ||||
|         label='next' | ||||
|         icon={ <NavigationArrowForward /> } | ||||
|         onClick={ this.onNextClick } /> | ||||
|     ); | ||||
|     const prevBtn = ( | ||||
|       <Button | ||||
|         key='prevStep' | ||||
|         label='prev' | ||||
|         icon={ <NavigationArrowBack /> } | ||||
|         onClick={ this.onPrevClick } /> | ||||
|     ); | ||||
| 
 | ||||
|     if (step === 0) { | ||||
|     if (step === STEP_DETAILS) { | ||||
|       return [ | ||||
|         cancelBtn, | ||||
|         <Button | ||||
|           key='postTransaction' | ||||
|           label='post transaction' | ||||
|           disabled={ !!(sending || hasError) } | ||||
|           icon={ <IdentityIcon address={ fromAddress } button /> } | ||||
|           onClick={ this.postTransaction } /> | ||||
|         gasEdit ? nextBtn : postBtn | ||||
|       ]; | ||||
|     } else if (step === 1) { | ||||
|     } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { | ||||
|       return [ | ||||
|         cancelBtn | ||||
|       ]; | ||||
|     } else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { | ||||
|       return [ | ||||
|         cancelBtn, | ||||
|         prevBtn, | ||||
|         postBtn | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     return [ | ||||
| @ -133,7 +180,8 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   renderStep () { | ||||
|     const { onFromAddressChange } = this.props; | ||||
|     const { step, busyState, gasLimitError, txhash, rejected } = this.state; | ||||
|     const { gasEdit, step, busyState, txhash, rejected } = this.state; | ||||
|     const { errorEstimated } = this.gasStore; | ||||
| 
 | ||||
|     if (rejected) { | ||||
|       return ( | ||||
| @ -144,23 +192,29 @@ class ExecuteContract extends Component { | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (step === 0) { | ||||
|     if (step === STEP_DETAILS) { | ||||
|       return ( | ||||
|         <DetailsStep | ||||
|           { ...this.props } | ||||
|           { ...this.state } | ||||
|           warning={ gasLimitError } | ||||
|           warning={ errorEstimated } | ||||
|           onAmountChange={ this.onAmountChange } | ||||
|           onFromAddressChange={ onFromAddressChange } | ||||
|           onFuncChange={ this.onFuncChange } | ||||
|           onGasEditClick={ this.onGasEditClick } | ||||
|           onValueChange={ this.onValueChange } /> | ||||
|       ); | ||||
|     } else if (step === 1) { | ||||
|     } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { | ||||
|       return ( | ||||
|         <BusyStep | ||||
|           title='The function execution is in progress' | ||||
|           state={ busyState } /> | ||||
|       ); | ||||
|     } else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { | ||||
|       return ( | ||||
|         <GasPriceEditor | ||||
|           store={ this.gasStore } /> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
| @ -171,6 +225,7 @@ class ExecuteContract extends Component { | ||||
|   } | ||||
| 
 | ||||
|   onAmountChange = (amount) => { | ||||
|     this.gasStore.setEthValue(amount); | ||||
|     this.setState({ amount }, this.estimateGas); | ||||
|   } | ||||
| 
 | ||||
| @ -221,7 +276,7 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   estimateGas = (_fromAddress) => { | ||||
|     const { api } = this.context; | ||||
|     const { fromAddress, gasLimit } = this.props; | ||||
|     const { fromAddress } = this.props; | ||||
|     const { amount, func, values } = this.state; | ||||
|     const options = { | ||||
|       gas: MAX_GAS_ESTIMATION, | ||||
| @ -237,18 +292,11 @@ class ExecuteContract extends Component { | ||||
|       .estimateGas(options, values) | ||||
|       .then((gasEst) => { | ||||
|         const gas = gasEst.mul(1.2); | ||||
|         let gasLimitError = null; | ||||
| 
 | ||||
|         if (gas.gte(MAX_GAS_ESTIMATION)) { | ||||
|           gasLimitError = ERRORS.gasException; | ||||
|         } else if (gas.gt(gasLimit)) { | ||||
|           gasLimitError = ERRORS.gasBlockLimit; | ||||
|         } | ||||
|         console.log(`estimateGas: received ${gasEst.toFormat(0)}, adjusted to ${gas.toFormat(0)}`); | ||||
| 
 | ||||
|         this.setState({ | ||||
|           gas, | ||||
|           gasLimitError | ||||
|         }); | ||||
|         this.gasStore.setEstimated(gasEst.toFixed(0)); | ||||
|         this.gasStore.setGas(gas.toFixed(0)); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.warn('estimateGas', error); | ||||
| @ -258,22 +306,20 @@ class ExecuteContract extends Component { | ||||
|   postTransaction = () => { | ||||
|     const { api, store } = this.context; | ||||
|     const { fromAddress } = this.props; | ||||
|     const { amount, func, values } = this.state; | ||||
|     const { amount, func, gasEdit, values } = this.state; | ||||
|     const steps = gasEdit ? STAGES_GAS : STAGES_BASIC; | ||||
|     const finalstep = steps.length - 1; | ||||
|     const options = { | ||||
|       gas: MAX_GAS_ESTIMATION, | ||||
|       gas: this.gasStore.gas, | ||||
|       gasPrice: this.gasStore.price, | ||||
|       from: fromAddress, | ||||
|       value: api.util.toWei(amount || 0) | ||||
|     }; | ||||
| 
 | ||||
|     this.setState({ sending: true, step: 1 }); | ||||
|     this.setState({ sending: true, step: gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS }); | ||||
| 
 | ||||
|     func | ||||
|       .estimateGas(options, values) | ||||
|       .then((gas) => { | ||||
|         options.gas = gas.mul(1.2).toFixed(0); | ||||
|         console.log(`estimateGas: received ${gas.toFormat(0)}, adjusted to ${gas.mul(1.2).toFormat(0)}`); | ||||
|         return func.postTransaction(options, values); | ||||
|       }) | ||||
|       .postTransaction(options, values) | ||||
|       .then((requestId) => { | ||||
|         this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); | ||||
| 
 | ||||
| @ -281,7 +327,7 @@ class ExecuteContract extends Component { | ||||
|           .pollMethod('parity_checkRequest', requestId) | ||||
|           .catch((error) => { | ||||
|             if (error.code === ERROR_CODES.REQUEST_REJECTED) { | ||||
|               this.setState({ rejected: true }); | ||||
|               this.setState({ rejected: true, step: finalstep }); | ||||
|               return false; | ||||
|             } | ||||
| 
 | ||||
| @ -289,13 +335,31 @@ class ExecuteContract extends Component { | ||||
|           }); | ||||
|       }) | ||||
|       .then((txhash) => { | ||||
|         this.setState({ sending: false, step: 2, txhash, busyState: 'Your transaction has been posted to the network' }); | ||||
|         this.setState({ sending: false, step: finalstep, txhash, busyState: 'Your transaction has been posted to the network' }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('postTransaction', error); | ||||
|         store.dispatch({ type: 'newError', error }); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   onGasEditClick = () => { | ||||
|     this.setState({ | ||||
|       gasEdit: !this.state.gasEdit | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onNextClick = () => { | ||||
|     this.setState({ | ||||
|       step: this.state.step + 1 | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onPrevClick = () => { | ||||
|     this.setState({ | ||||
|       step: this.state.step - 1 | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function mapStateToProps (state) { | ||||
|  | ||||
| @ -21,9 +21,8 @@ import { sha3 } from '~/api/util/sha3'; | ||||
| import Contracts from '~/contracts'; | ||||
| 
 | ||||
| import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification'; | ||||
| import { postToServer } from '../../3rdparty/sms-verification'; | ||||
| import checkIfTxFailed from '../../util/check-if-tx-failed'; | ||||
| import waitForConfirmations from '../../util/wait-for-block-confirmations'; | ||||
| import { postToServer } from '~/3rdparty/sms-verification'; | ||||
| import { checkIfTxFailed, waitForConfirmations } from '~/util/tx'; | ||||
| 
 | ||||
| export const LOADING = 'fetching-contract'; | ||||
| export const QUERY_DATA = 'query-data'; | ||||
|  | ||||
| @ -20,7 +20,7 @@ import SaveIcon from 'material-ui/svg-icons/content/save'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| 
 | ||||
| import { Button, Modal, Editor, Form, Input } from '~/ui'; | ||||
| import { ERRORS, validateName } from '../../util/validation'; | ||||
| import { ERRORS, validateName } from '~/util/validation'; | ||||
| 
 | ||||
| import styles from './saveContract.css'; | ||||
| 
 | ||||
|  | ||||
| @ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| 
 | ||||
| import { Button, IdentityIcon, Modal } from '~/ui'; | ||||
| import initShapeshift from '../../3rdparty/shapeshift'; | ||||
| import initShapeshift from '~/3rdparty/shapeshift'; | ||||
| import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png'; | ||||
| 
 | ||||
| import AwaitingDepositStep from './AwaitingDepositStep'; | ||||
|  | ||||
| @ -16,96 +16,28 @@ | ||||
| 
 | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import Form, { Input } from '~/ui/Form'; | ||||
| import GasPriceSelector from '../GasPriceSelector'; | ||||
| 
 | ||||
| import styles from '../transfer.css'; | ||||
| import { GasPriceEditor, Form, Input } from '~/ui'; | ||||
| 
 | ||||
| export default class Extras extends Component { | ||||
|   static propTypes = { | ||||
|     isEth: PropTypes.bool, | ||||
|     data: PropTypes.string, | ||||
|     dataError: PropTypes.string, | ||||
|     gas: PropTypes.string, | ||||
|     gasEst: PropTypes.string, | ||||
|     gasError: PropTypes.string, | ||||
|     gasPrice: PropTypes.oneOfType([ | ||||
|       PropTypes.string, | ||||
|       PropTypes.object | ||||
|     ]), | ||||
|     gasPriceDefault: PropTypes.string, | ||||
|     gasPriceError: PropTypes.string, | ||||
|     gasPriceHistogram: PropTypes.object, | ||||
|     total: PropTypes.string, | ||||
|     totalError: PropTypes.string, | ||||
|     onChange: PropTypes.func.isRequired | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     gasStore: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { gas, gasPrice, gasError, gasEst, gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.props; | ||||
| 
 | ||||
|     const gasLabel = `gas amount (estimated: ${gasEst})`; | ||||
|     const priceLabel = `gas price (current: ${gasPriceDefault})`; | ||||
|     const { gasStore, onChange } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
| 
 | ||||
|         { this.renderData() } | ||||
| 
 | ||||
|         <div className={ styles.columns }> | ||||
|           <div style={ { flex: 65 } }> | ||||
|             <GasPriceSelector | ||||
|               gasPriceHistogram={ gasPriceHistogram } | ||||
|               gasPrice={ gasPrice } | ||||
|               onChange={ this.onEditGasPrice } | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div | ||||
|             className={ styles.row } | ||||
|             style={ { | ||||
|               flex: 35, paddingLeft: '1rem', | ||||
|               justifyContent: 'space-around', | ||||
|               paddingBottom: 12 | ||||
|             } } | ||||
|           > | ||||
|             <div className={ styles.row }> | ||||
|               <Input | ||||
|                 label={ gasLabel } | ||||
|                 hint='the amount of gas to use for the transaction' | ||||
|                 error={ gasError } | ||||
|                 value={ gas } | ||||
|                 onChange={ this.onEditGas } /> | ||||
| 
 | ||||
|               <Input | ||||
|                 label={ priceLabel } | ||||
|                 hint='the price of gas to use for the transaction' | ||||
|                 error={ gasPriceError } | ||||
|                 value={ (gasPrice || '').toString() } | ||||
|                 onChange={ this.onEditGasPrice } /> | ||||
|             </div> | ||||
| 
 | ||||
|             <div className={ styles.row }> | ||||
|               <Input | ||||
|                 disabled | ||||
|                 label='total transaction amount' | ||||
|                 hint='the total amount of the transaction' | ||||
|                 error={ totalError } | ||||
|                 value={ `${total} ETH` } /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div> | ||||
|           <p className={ styles.gasPriceDesc }> | ||||
|             You can choose the gas price based on the | ||||
|             distribution of recent included transactions' gas prices. | ||||
|             The lower the gas price is, the cheaper the transaction will | ||||
|             be. The higher the gas price is, the faster it should | ||||
|             get mined by the network. | ||||
|           </p> | ||||
|         </div> | ||||
| 
 | ||||
|         <GasPriceEditor | ||||
|           store={ gasStore } | ||||
|           onChange={ onChange } /> | ||||
|       </Form> | ||||
|     ); | ||||
|   } | ||||
| @ -129,14 +61,6 @@ export default class Extras extends Component { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   onEditGas = (event) => { | ||||
|     this.props.onChange('gas', event.target.value); | ||||
|   } | ||||
| 
 | ||||
|   onEditGasPrice = (event, value) => { | ||||
|     this.props.onChange('gasPrice', value); | ||||
|   } | ||||
| 
 | ||||
|   onEditData = (event) => { | ||||
|     this.props.onChange('data', event.target.value); | ||||
|   } | ||||
|  | ||||
| @ -23,7 +23,8 @@ import { bytesToHex } from '~/api/util/format'; | ||||
| import Contract from '~/api/contract'; | ||||
| import ERRORS from './errors'; | ||||
| import { ERROR_CODES } from '~/api/transport/error'; | ||||
| import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| import GasPriceStore from '~/ui/GasPriceEditor/store'; | ||||
| 
 | ||||
| const TITLES = { | ||||
|   transfer: 'transfer details', | ||||
| @ -48,14 +49,6 @@ export default class TransferStore { | ||||
|   @observable data = ''; | ||||
|   @observable dataError = null; | ||||
| 
 | ||||
|   @observable gas = DEFAULT_GAS; | ||||
|   @observable gasError = null; | ||||
| 
 | ||||
|   @observable gasEst = '0'; | ||||
|   @observable gasLimitError = null; | ||||
|   @observable gasPrice = DEFAULT_GASPRICE; | ||||
|   @observable gasPriceError = null; | ||||
| 
 | ||||
|   @observable recipient = ''; | ||||
|   @observable recipientError = ERRORS.requireRecipient; | ||||
| 
 | ||||
| @ -68,11 +61,8 @@ export default class TransferStore { | ||||
|   @observable value = '0.0'; | ||||
|   @observable valueError = null; | ||||
| 
 | ||||
|   gasPriceHistogram = {}; | ||||
| 
 | ||||
|   account = null; | ||||
|   balance = null; | ||||
|   gasLimit = null; | ||||
|   onClose = null; | ||||
| 
 | ||||
|   senders = null; | ||||
| @ -81,6 +71,8 @@ export default class TransferStore { | ||||
|   isWallet = false; | ||||
|   wallet = null; | ||||
| 
 | ||||
|   gasStore = null; | ||||
| 
 | ||||
|   @computed get steps () { | ||||
|     const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); | ||||
| 
 | ||||
| @ -93,7 +85,7 @@ export default class TransferStore { | ||||
| 
 | ||||
|   @computed get isValid () { | ||||
|     const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; | ||||
|     const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError; | ||||
|     const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.totalError; | ||||
|     const verifyValid = !this.passwordError; | ||||
| 
 | ||||
|     switch (this.stage) { | ||||
| @ -118,11 +110,12 @@ export default class TransferStore { | ||||
|     const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props; | ||||
|     this.account = account; | ||||
|     this.balance = balance; | ||||
|     this.gasLimit = gasLimit; | ||||
|     this.onClose = onClose; | ||||
|     this.isWallet = account && account.wallet; | ||||
|     this.newError = newError; | ||||
| 
 | ||||
|     this.gasStore = new GasPriceStore(api, gasLimit); | ||||
| 
 | ||||
|     if (this.isWallet) { | ||||
|       this.wallet = props.wallet; | ||||
|       this.walletContract = new Contract(this.api, walletAbi); | ||||
| @ -179,26 +172,6 @@ export default class TransferStore { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @action getDefaults = () => { | ||||
|     Promise | ||||
|       .all([ | ||||
|         this.api.parity.gasPriceHistogram(), | ||||
|         this.api.eth.gasPrice() | ||||
|       ]) | ||||
|       .then(([gasPriceHistogram, gasPrice]) => { | ||||
|         transaction(() => { | ||||
|           this.gasPrice = gasPrice.toString(); | ||||
|           this.gasPriceDefault = gasPrice.toFormat(); | ||||
|           this.gasPriceHistogram = gasPriceHistogram; | ||||
| 
 | ||||
|           this.recalculate(); | ||||
|         }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.warn('getDefaults', error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   @action onSend = () => { | ||||
|     this.onNext(); | ||||
|     this.sending = true; | ||||
| @ -281,25 +254,11 @@ export default class TransferStore { | ||||
|   } | ||||
| 
 | ||||
|   @action _onUpdateGas = (gas) => { | ||||
|     const gasError = this._validatePositiveNumber(gas); | ||||
| 
 | ||||
|     transaction(() => { | ||||
|       this.gas = gas; | ||||
|       this.gasError = gasError; | ||||
| 
 | ||||
|       this.recalculate(); | ||||
|     }); | ||||
|     this.recalculate(); | ||||
|   } | ||||
| 
 | ||||
|   @action _onUpdateGasPrice = (gasPrice) => { | ||||
|     const gasPriceError = this._validatePositiveNumber(gasPrice); | ||||
| 
 | ||||
|     transaction(() => { | ||||
|       this.gasPrice = gasPrice; | ||||
|       this.gasPriceError = gasPriceError; | ||||
| 
 | ||||
|       this.recalculate(); | ||||
|     }); | ||||
|     this.recalculate(); | ||||
|   } | ||||
| 
 | ||||
|   @action _onUpdateRecipient = (recipient) => { | ||||
| @ -362,7 +321,7 @@ export default class TransferStore { | ||||
| 
 | ||||
|   @action recalculateGas = () => { | ||||
|     if (!this.isValid) { | ||||
|       this.gas = 0; | ||||
|       this.gasStore.setGas('0'); | ||||
|       return this.recalculate(); | ||||
|     } | ||||
| 
 | ||||
| @ -370,28 +329,20 @@ export default class TransferStore { | ||||
|       .estimateGas() | ||||
|       .then((gasEst) => { | ||||
|         let gas = gasEst; | ||||
|         let gasLimitError = null; | ||||
| 
 | ||||
|         if (gas.gt(DEFAULT_GAS)) { | ||||
|           gas = gas.mul(1.2); | ||||
|         } | ||||
| 
 | ||||
|         if (gas.gte(MAX_GAS_ESTIMATION)) { | ||||
|           gasLimitError = ERRORS.gasException; | ||||
|         } else if (gas.gt(this.gasLimit)) { | ||||
|           gasLimitError = ERRORS.gasBlockLimit; | ||||
|         } | ||||
| 
 | ||||
|         transaction(() => { | ||||
|           this.gas = gas.toFixed(0); | ||||
|           this.gasEst = gasEst.toFormat(); | ||||
|           this.gasLimitError = gasLimitError; | ||||
|           this.gasStore.setEstimated(gasEst.toFixed(0)); | ||||
|           this.gasStore.setGas(gas.toFixed(0)); | ||||
| 
 | ||||
|           this.recalculate(); | ||||
|         }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('etimateGas', error); | ||||
|         console.warn('etimateGas', error); | ||||
|         this.recalculate(); | ||||
|       }); | ||||
|   } | ||||
| @ -411,9 +362,9 @@ export default class TransferStore { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const { gas, gasPrice, tag, valueAll, isEth, isWallet } = this; | ||||
|     const { tag, valueAll, isEth, isWallet } = this; | ||||
| 
 | ||||
|     const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0)); | ||||
|     const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0)); | ||||
| 
 | ||||
|     const availableEth = new BigNumber(balance.tokens[0].value); | ||||
| 
 | ||||
| @ -453,10 +404,12 @@ export default class TransferStore { | ||||
|     } | ||||
| 
 | ||||
|     transaction(() => { | ||||
|       this.total = this.api.util.fromWei(totalEth).toString(); | ||||
|       this.total = this.api.util.fromWei(totalEth).toFixed(); | ||||
|       this.totalError = totalError; | ||||
|       this.value = value; | ||||
|       this.valueError = valueError; | ||||
|       this.gasStore.setErrorTotal(totalError); | ||||
|       this.gasStore.setEthValue(totalEth); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| @ -522,8 +475,8 @@ export default class TransferStore { | ||||
|     }; | ||||
| 
 | ||||
|     if (!gas) { | ||||
|       options.gas = this.gas; | ||||
|       options.gasPrice = this.gasPrice; | ||||
|       options.gas = this.gasStore.gas; | ||||
|       options.gasPrice = this.gasStore.price; | ||||
|     } else { | ||||
|       options.gas = MAX_GAS_ESTIMATION; | ||||
|     } | ||||
|  | ||||
| @ -144,15 +144,6 @@ | ||||
|   font-size: 1.2rem; | ||||
| } | ||||
| 
 | ||||
| .chart { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .gasPriceDesc { | ||||
|   font-size: 0.9em; | ||||
| } | ||||
| 
 | ||||
| .warning { | ||||
|   border-radius: 0.5em; | ||||
|   background: #f80; | ||||
|  | ||||
| @ -56,10 +56,6 @@ class Transfer extends Component { | ||||
| 
 | ||||
|   store = new TransferStore(this.context.api, this.props); | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.store.getDefaults(); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { stage, extras, steps } = this.store; | ||||
| 
 | ||||
| @ -186,27 +182,20 @@ class Transfer extends Component { | ||||
|   } | ||||
| 
 | ||||
|   renderExtrasPage () { | ||||
|     if (!this.store.gasPriceHistogram) { | ||||
|     if (!this.store.gasStore.histogram) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store; | ||||
|     const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store; | ||||
|     const { isEth, data, dataError, total, totalError } = this.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Extras | ||||
|         isEth={ isEth } | ||||
|         data={ data } | ||||
|         dataError={ dataError } | ||||
|         gas={ gas } | ||||
|         gasEst={ gasEst } | ||||
|         gasError={ gasError } | ||||
|         gasPrice={ gasPrice } | ||||
|         gasPriceDefault={ gasPriceDefault } | ||||
|         gasPriceError={ gasPriceError } | ||||
|         gasPriceHistogram={ gasPriceHistogram } | ||||
|         total={ total } | ||||
|         totalError={ totalError } | ||||
|         gasStore={ this.store.gasStore } | ||||
|         onChange={ this.store.onUpdateDetails } /> | ||||
|     ); | ||||
|   } | ||||
| @ -263,15 +252,15 @@ class Transfer extends Component { | ||||
|   } | ||||
| 
 | ||||
|   renderWarning () { | ||||
|     const { gasLimitError } = this.store; | ||||
|     const { errorEstimated } = this.store.gasStore; | ||||
| 
 | ||||
|     if (!gasLimitError) { | ||||
|     if (!errorEstimated) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.warning }> | ||||
|         { gasLimitError } | ||||
|         { errorEstimated } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -17,12 +17,10 @@ | ||||
| import { isEqual, uniq } from 'lodash'; | ||||
| 
 | ||||
| import Contract from '~/api/contract'; | ||||
| import { wallet as WALLET_ABI } from '~/contracts/abi'; | ||||
| import { bytesToHex, toHex } from '~/api/util/format'; | ||||
| 
 | ||||
| import { ERROR_CODES } from '~/api/transport/error'; | ||||
| import { MAX_GAS_ESTIMATION } from '../../util/constants'; | ||||
| 
 | ||||
| import { wallet as WALLET_ABI } from '~/contracts/abi'; | ||||
| import { MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| import WalletsUtils from '~/util/wallets'; | ||||
| 
 | ||||
| import { newError } from '~/ui/Errors/actions'; | ||||
|  | ||||
| @ -22,12 +22,11 @@ import IconButton from 'material-ui/IconButton'; | ||||
| import AddIcon from 'material-ui/svg-icons/content/add'; | ||||
| import RemoveIcon from 'material-ui/svg-icons/content/remove'; | ||||
| 
 | ||||
| import { fromWei, toWei } from '~/api/util/wei'; | ||||
| import Input from '~/ui/Form/Input'; | ||||
| import InputAddressSelect from '~/ui/Form/InputAddressSelect'; | ||||
| import Select from '~/ui/Form/Select'; | ||||
| 
 | ||||
| import { ABI_TYPES } from '~/util/abi'; | ||||
| import { fromWei, toWei } from '~/api/util/wei'; | ||||
| 
 | ||||
| import styles from './typedInput.css'; | ||||
| 
 | ||||
|  | ||||
| @ -15,3 +15,13 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .chart { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .columns { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
| } | ||||
| @ -29,10 +29,7 @@ import { | ||||
| import Slider from 'material-ui/Slider'; | ||||
| import BigNumber from 'bignumber.js'; | ||||
| 
 | ||||
| import componentStyles from './gasPriceSelector.css'; | ||||
| import mainStyles from '../transfer.css'; | ||||
| 
 | ||||
| const styles = Object.assign({}, mainStyles, componentStyles); | ||||
| import styles from './gasPriceSelector.css'; | ||||
| 
 | ||||
| const COLORS = { | ||||
|   default: 'rgba(255, 99, 132, 0.2)', | ||||
| @ -194,10 +191,7 @@ class CustomizedShape extends Component { | ||||
| 
 | ||||
| class CustomTooltip extends Component { | ||||
|   static propTypes = { | ||||
|     gasPriceHistogram: PropTypes.shape({ | ||||
|       bucketBounds: PropTypes.array.isRequired, | ||||
|       counts: PropTypes.array.isRequired | ||||
|     }).isRequired, | ||||
|     gasPriceHistogram: PropTypes.object.isRequired, | ||||
|     type: PropTypes.string, | ||||
|     payload: PropTypes.array, | ||||
|     label: PropTypes.number, | ||||
| @ -231,12 +225,16 @@ class CustomTooltip extends Component { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const TOOL_STYLE = { | ||||
|   color: 'rgba(255,255,255,0.5)', | ||||
|   backgroundColor: 'rgba(0, 0, 0, 0.75)', | ||||
|   padding: '0 0.5em', | ||||
|   fontSize: '0.75em' | ||||
| }; | ||||
| 
 | ||||
| export default class GasPriceSelector extends Component { | ||||
|   static propTypes = { | ||||
|     gasPriceHistogram: PropTypes.shape({ | ||||
|       bucketBounds: PropTypes.array.isRequired, | ||||
|       counts: PropTypes.array.isRequired | ||||
|     }).isRequired, | ||||
|     gasPriceHistogram: PropTypes.object.isRequired, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
| 
 | ||||
|     gasPrice: PropTypes.oneOfType([ | ||||
| @ -287,21 +285,23 @@ export default class GasPriceSelector extends Component { | ||||
|   renderSlider () { | ||||
|     const { sliderValue } = this.state; | ||||
| 
 | ||||
|     return (<div className={ styles.columns }> | ||||
|       <Slider | ||||
|         min={ 0 } | ||||
|         max={ 1 } | ||||
|         value={ sliderValue } | ||||
|         onChange={ this.onEditGasPriceSlider } | ||||
|         style={ { | ||||
|           flex: 1, | ||||
|           padding: '0 0.3em' | ||||
|         } } | ||||
|         sliderStyle={ { | ||||
|           marginBottom: 12 | ||||
|         } } | ||||
|       /> | ||||
|     </div>); | ||||
|     return ( | ||||
|       <div className={ styles.columns }> | ||||
|         <Slider | ||||
|           min={ 0 } | ||||
|           max={ 1 } | ||||
|           value={ sliderValue } | ||||
|           onChange={ this.onEditGasPriceSlider } | ||||
|           style={ { | ||||
|             flex: 1, | ||||
|             padding: '0 0.3em' | ||||
|           } } | ||||
|           sliderStyle={ { | ||||
|             marginBottom: 12 | ||||
|           } } | ||||
|         /> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderChart () { | ||||
| @ -316,85 +316,83 @@ export default class GasPriceSelector extends Component { | ||||
|     const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); | ||||
|     const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); | ||||
| 
 | ||||
|     return (<div className={ styles.columns }> | ||||
|       <div style={ { flex: 1, height } }> | ||||
|         <div className={ styles.chart }> | ||||
|           <ResponsiveContainer | ||||
|             height={ height } | ||||
|           > | ||||
|             <ScatterChart | ||||
|               margin={ { top: 0, right: 0, left: 0, bottom: 0 } } | ||||
|     return ( | ||||
|       <div className={ styles.columns }> | ||||
|         <div style={ { flex: 1, height } }> | ||||
|           <div className={ styles.chart }> | ||||
|             <ResponsiveContainer | ||||
|               height={ height } | ||||
|             > | ||||
|               <Scatter | ||||
|                 data={ [ | ||||
|                   { x: sliderValue, y: 0 }, | ||||
|                   { x: sliderValue, y: selectedCount }, | ||||
|                   { x: sliderValue, y: chartData.yDomain[1] } | ||||
|                 ] } | ||||
|                 shape={ <CustomizedShape showValue={ selectedCount } /> } | ||||
|                 line | ||||
|                 isAnimationActive={ false } | ||||
|               /> | ||||
|               <ScatterChart | ||||
|                 margin={ { top: 0, right: 0, left: 0, bottom: 0 } } | ||||
|               > | ||||
|                 <Scatter | ||||
|                   data={ [ | ||||
|                     { x: sliderValue, y: 0 }, | ||||
|                     { x: sliderValue, y: selectedCount }, | ||||
|                     { x: sliderValue, y: chartData.yDomain[1] } | ||||
|                   ] } | ||||
|                   shape={ <CustomizedShape showValue={ selectedCount } /> } | ||||
|                   line | ||||
|                   isAnimationActive={ false } | ||||
|                 /> | ||||
| 
 | ||||
|               <XAxis | ||||
|                 hide | ||||
|                 height={ 0 } | ||||
|                 dataKey='x' | ||||
|                 domain={ [0, 1] } | ||||
|               /> | ||||
|               <YAxis | ||||
|                 hide | ||||
|                 width={ 0 } | ||||
|                 dataKey='y' | ||||
|                 domain={ chartData.yDomain } | ||||
|               /> | ||||
|             </ScatterChart> | ||||
|           </ResponsiveContainer> | ||||
|         </div> | ||||
|                 <XAxis | ||||
|                   hide | ||||
|                   height={ 0 } | ||||
|                   dataKey='x' | ||||
|                   domain={ [0, 1] } | ||||
|                 /> | ||||
|                 <YAxis | ||||
|                   hide | ||||
|                   width={ 0 } | ||||
|                   dataKey='y' | ||||
|                   domain={ chartData.yDomain } | ||||
|                 /> | ||||
|               </ScatterChart> | ||||
|             </ResponsiveContainer> | ||||
|           </div> | ||||
| 
 | ||||
|         <div className={ styles.chart }> | ||||
|           <ResponsiveContainer | ||||
|             height={ height } | ||||
|           > | ||||
|             <BarChart | ||||
|               data={ chartData.values } | ||||
|               margin={ { top: 0, right: 0, left: 0, bottom: 0 } } | ||||
|               barCategoryGap={ 1 } | ||||
|               ref='barChart' | ||||
|           <div className={ styles.chart }> | ||||
|             <ResponsiveContainer | ||||
|               height={ height } | ||||
|             > | ||||
|               <Bar | ||||
|                 dataKey='value' | ||||
|                 stroke={ COLORS.line } | ||||
|                 onClick={ this.onClickGasPrice } | ||||
|                 shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> } | ||||
|               /> | ||||
|               <BarChart | ||||
|                 data={ chartData.values } | ||||
|                 margin={ { top: 0, right: 0, left: 0, bottom: 0 } } | ||||
|                 barCategoryGap={ 1 } | ||||
|                 ref='barChart' | ||||
|               > | ||||
|                 <Bar | ||||
|                   dataKey='value' | ||||
|                   stroke={ COLORS.line } | ||||
|                   onClick={ this.onClickGasPrice } | ||||
|                   shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> } | ||||
|                 /> | ||||
| 
 | ||||
|               <Tooltip | ||||
|                 wrapperStyle={ { | ||||
|                   backgroundColor: 'rgba(0, 0, 0, 0.75)', | ||||
|                   padding: '0 0.5em', | ||||
|                   fontSize: '0.9em' | ||||
|                 } } | ||||
|                 cursor={ this.renderCustomCursor() } | ||||
|                 content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> } | ||||
|               /> | ||||
|                 <Tooltip | ||||
|                   wrapperStyle={ TOOL_STYLE } | ||||
|                   cursor={ this.renderCustomCursor() } | ||||
|                   content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> } | ||||
|                 /> | ||||
| 
 | ||||
|               <XAxis | ||||
|                 hide | ||||
|                 dataKey='index' | ||||
|                 type='category' | ||||
|                 domain={ chartData.xDomain } | ||||
|               /> | ||||
|               <YAxis | ||||
|                 hide | ||||
|                 type='number' | ||||
|                 domain={ chartData.yDomain } | ||||
|               /> | ||||
|             </BarChart> | ||||
|           </ResponsiveContainer> | ||||
|                 <XAxis | ||||
|                   hide | ||||
|                   dataKey='index' | ||||
|                   type='category' | ||||
|                   domain={ chartData.xDomain } | ||||
|                 /> | ||||
|                 <YAxis | ||||
|                   hide | ||||
|                   type='number' | ||||
|                   domain={ chartData.yDomain } | ||||
|                 /> | ||||
|               </BarChart> | ||||
|             </ResponsiveContainer> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div>); | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderCustomCursor = () => { | ||||
| @ -14,11 +14,36 @@ | ||||
| /* You should have received a copy of the GNU General Public License | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| .signer { | ||||
| 
 | ||||
| .columns { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
| .graphColumn { | ||||
|   flex: 65; | ||||
| } | ||||
| 
 | ||||
| .mainContainer { | ||||
| .editColumn { | ||||
|   flex: 35; | ||||
|   padding-left: 1em; | ||||
|   justify-ontent: space-around; | ||||
|   padding-bottom: 12; | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
|   flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| .gasPriceDesc { | ||||
|   font-size: 0.75em; | ||||
|   opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .row { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
|   flex-direction: column; | ||||
| } | ||||
							
								
								
									
										108
									
								
								js/src/ui/GasPriceEditor/gasPriceEditor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								js/src/ui/GasPriceEditor/gasPriceEditor.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| // Copyright 2015, 2016 Ethcore (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import BigNumber from 'bignumber.js'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { observer } from 'mobx-react'; | ||||
| 
 | ||||
| import Input from '../Form/Input'; | ||||
| import GasPriceSelector from './GasPriceSelector'; | ||||
| import Store from './store'; | ||||
| 
 | ||||
| import styles from './gasPriceEditor.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class GasPriceEditor extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     store: PropTypes.object.isRequired, | ||||
|     onChange: PropTypes.func | ||||
|   } | ||||
| 
 | ||||
|   static Store = Store; | ||||
| 
 | ||||
|   render () { | ||||
|     const { api } = this.context; | ||||
|     const { store } = this.props; | ||||
|     const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice, errorTotal, totalValue } = store; | ||||
| 
 | ||||
|     const eth = api.util.fromWei(totalValue).toFormat(); | ||||
|     const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`; | ||||
|     const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.columns }> | ||||
|         <div className={ styles.graphColumn }> | ||||
|           <GasPriceSelector | ||||
|             gasPriceHistogram={ histogram } | ||||
|             gasPrice={ price } | ||||
|             onChange={ this.onEditGasPrice } /> | ||||
|           <div className={ styles.gasPriceDesc }> | ||||
|             You can choose the gas price based on the | ||||
|             distribution of recent included transaction gas prices. | ||||
|             The lower the gas price is, the cheaper the transaction will | ||||
|             be. The higher the gas price is, the faster it should | ||||
|             get mined by the network. | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className={ styles.editColumn }> | ||||
|           <div className={ styles.row }> | ||||
|             <Input | ||||
|               label={ gasLabel } | ||||
|               hint='the amount of gas to use for the transaction' | ||||
|               error={ errorGas } | ||||
|               value={ gas } | ||||
|               onChange={ this.onEditGas } /> | ||||
| 
 | ||||
|             <Input | ||||
|               label={ priceLabel } | ||||
|               hint='the price of gas to use for the transaction' | ||||
|               error={ errorPrice } | ||||
|               value={ price } | ||||
|               onChange={ this.onEditGasPrice } /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className={ styles.row }> | ||||
|             <Input | ||||
|               disabled | ||||
|               label='total transaction amount' | ||||
|               hint='the total amount of the transaction' | ||||
|               error={ errorTotal } | ||||
|               value={ `${eth} ETH` } /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   onEditGas = (event, gas) => { | ||||
|     const { store, onChange } = this.props; | ||||
| 
 | ||||
|     store.setGas(gas); | ||||
|     onChange && onChange('gas', gas); | ||||
|   } | ||||
| 
 | ||||
|   onEditGasPrice = (event, price) => { | ||||
|     const { store, onChange } = this.props; | ||||
| 
 | ||||
|     store.setPrice(price); | ||||
|     onChange && onChange('gasPrice', price); | ||||
|   } | ||||
| } | ||||
| @ -14,6 +14,4 @@ | ||||
| // 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 (chain) => { | ||||
|   return chain === 'morden' || chain === 'ropsten' || chain === 'testnet'; | ||||
| }; | ||||
| export default from './gasPriceEditor'; | ||||
							
								
								
									
										123
									
								
								js/src/ui/GasPriceEditor/store.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								js/src/ui/GasPriceEditor/store.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| // Copyright 2015, 2016 Ethcore (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import BigNumber from 'bignumber.js'; | ||||
| import { action, computed, observable, transaction } from 'mobx'; | ||||
| 
 | ||||
| import { ERRORS, validatePositiveNumber } from '~/util/validation'; | ||||
| import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| 
 | ||||
| export default class GasPriceEditor { | ||||
|   @observable errorEstimated = null; | ||||
|   @observable errorGas = null; | ||||
|   @observable errorPrice = null; | ||||
|   @observable errorTotal = null; | ||||
|   @observable estimated = DEFAULT_GAS; | ||||
|   @observable histogram = null; | ||||
|   @observable price = DEFAULT_GASPRICE; | ||||
|   @observable priceDefault = DEFAULT_GASPRICE; | ||||
|   @observable gas = DEFAULT_GAS; | ||||
|   @observable gasLimit = 0; | ||||
|   @observable weiValue = '0'; | ||||
| 
 | ||||
|   constructor (api, gasLimit, loadDefaults = true) { | ||||
|     this._api = api; | ||||
|     this.gasLimit = gasLimit; | ||||
| 
 | ||||
|     if (loadDefaults) { | ||||
|       this.loadDefaults(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @computed get totalValue () { | ||||
|     try { | ||||
|       return new BigNumber(this.gas).mul(this.price).add(this.weiValue); | ||||
|     } catch (error) { | ||||
|       return new BigNumber(0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @action setErrorTotal = (errorTotal) => { | ||||
|     this.errorTotal = errorTotal; | ||||
|   } | ||||
| 
 | ||||
|   @action setEstimated = (estimated) => { | ||||
|     transaction(() => { | ||||
|       const bn = new BigNumber(estimated); | ||||
| 
 | ||||
|       this.estimated = estimated; | ||||
| 
 | ||||
|       if (bn.gte(MAX_GAS_ESTIMATION)) { | ||||
|         this.errorEstimated = ERRORS.gasException; | ||||
|       } else if (bn.gte(this.gasLimit)) { | ||||
|         this.errorEstimated = ERRORS.gasBlockLimit; | ||||
|       } else { | ||||
|         this.errorEstimated = null; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setEthValue = (weiValue) => { | ||||
|     this.weiValue = weiValue; | ||||
|   } | ||||
| 
 | ||||
|   @action setHistogram = (gasHistogram) => { | ||||
|     this.histogram = gasHistogram; | ||||
|   } | ||||
| 
 | ||||
|   @action setPrice = (price) => { | ||||
|     transaction(() => { | ||||
|       this.errorPrice = validatePositiveNumber(price).numberError; | ||||
|       this.price = price; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setGas = (gas) => { | ||||
|     transaction(() => { | ||||
|       const { numberError } = validatePositiveNumber(gas); | ||||
|       const bn = new BigNumber(gas); | ||||
| 
 | ||||
|       this.gas = gas; | ||||
| 
 | ||||
|       if (numberError) { | ||||
|         this.errorGas = numberError; | ||||
|       } else if (bn.gte(this.gasLimit)) { | ||||
|         this.errorGas = ERRORS.gasBlockLimit; | ||||
|       } else { | ||||
|         this.errorGas = null; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action loadDefaults () { | ||||
|     Promise | ||||
|       .all([ | ||||
|         this._api.parity.gasPriceHistogram(), | ||||
|         this._api.eth.gasPrice() | ||||
|       ]) | ||||
|       .then(([gasPriceHistogram, gasPrice]) => { | ||||
|         transaction(() => { | ||||
|           this.setPrice(gasPrice.toFixed(0)); | ||||
|           this.setHistogram(gasPriceHistogram); | ||||
| 
 | ||||
|           this.priceDefault = gasPrice.toFixed(); | ||||
|         }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.warn('getDefaults', error); | ||||
|       }); | ||||
|   } | ||||
| } | ||||
| @ -20,7 +20,7 @@ import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import { LinearProgress } from 'material-ui'; | ||||
| 
 | ||||
| import { txLink } from '../../3rdparty/etherscan/links'; | ||||
| import { txLink } from '~/3rdparty/etherscan/links'; | ||||
| import ShortenedHash from '../ShortenedHash'; | ||||
| 
 | ||||
| import styles from './txHash.css'; | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import { observer } from 'mobx-react'; | ||||
| 
 | ||||
| import { txLink, addressLink } from '../../3rdparty/etherscan/links'; | ||||
| import { txLink, addressLink } from '~/3rdparty/etherscan/links'; | ||||
| 
 | ||||
| import IdentityIcon from '../IdentityIcon'; | ||||
| import IdentityName from '../IdentityName'; | ||||
|  | ||||
| @ -31,6 +31,7 @@ import CopyToClipboard from './CopyToClipboard'; | ||||
| import Editor from './Editor'; | ||||
| import Errors from './Errors'; | ||||
| import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; | ||||
| import GasPriceEditor from './GasPriceEditor'; | ||||
| import IdentityIcon from './IdentityIcon'; | ||||
| import IdentityName from './IdentityName'; | ||||
| import Loading from './Loading'; | ||||
| @ -67,7 +68,7 @@ export { | ||||
|   Errors, | ||||
|   Form, | ||||
|   FormWrap, | ||||
|   TypedInput, | ||||
|   GasPriceEditor, | ||||
|   Input, | ||||
|   InputAddress, | ||||
|   InputAddressSelect, | ||||
| @ -91,5 +92,6 @@ export { | ||||
|   Tooltip, | ||||
|   Tooltips, | ||||
|   TxHash, | ||||
|   TxList | ||||
|   TxList, | ||||
|   TypedInput | ||||
| }; | ||||
|  | ||||
| @ -1,28 +0,0 @@ | ||||
| // Copyright 2015, 2016 Ethcore (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| const checkIfTxFailed = (api, tx, gasSent) => { | ||||
|   return api.pollMethod('eth_getTransactionReceipt', tx) | ||||
|   .then((receipt) => { | ||||
|     // TODO: Right now, there's no way to tell wether the EVM code crashed.
 | ||||
|     // Because you usually send a bit more gas than estimated (to make sure
 | ||||
|     // it gets mined quickly), we transaction probably failed if all the gas
 | ||||
|     // has been used up.
 | ||||
|     return receipt.gasUsed.eq(gasSent); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export default checkIfTxFailed; | ||||
| @ -18,7 +18,18 @@ const isValidReceipt = (receipt) => { | ||||
|   return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); | ||||
| }; | ||||
| 
 | ||||
| const waitForConfirmations = (api, tx, confirmations) => { | ||||
| export function checkIfTxFailed (api, tx, gasSent) { | ||||
|   return api.pollMethod('eth_getTransactionReceipt', tx) | ||||
|   .then((receipt) => { | ||||
|     // TODO: Right now, there's no way to tell wether the EVM code crashed.
 | ||||
|     // Because you usually send a bit more gas than estimated (to make sure
 | ||||
|     // it gets mined quickly), we transaction probably failed if all the gas
 | ||||
|     // has been used up.
 | ||||
|     return receipt.gasUsed.eq(gasSent); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function waitForConfirmations (api, tx, confirmations) { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt) | ||||
|     .then((receipt) => { | ||||
| @ -39,6 +50,4 @@ const waitForConfirmations = (api, tx, confirmations) => { | ||||
|       .catch(reject); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export default waitForConfirmations; | ||||
| } | ||||
| @ -20,6 +20,7 @@ import util from '~/api/util'; | ||||
| 
 | ||||
| export const ERRORS = { | ||||
|   invalidAddress: 'address is an invalid network address', | ||||
|   invalidAmount: 'the supplied amount should be a valid positive number', | ||||
|   duplicateAddress: 'the address is already in your address book', | ||||
|   invalidChecksum: 'address has failed the checksum formatting', | ||||
|   invalidName: 'name should not be blank and longer than 2', | ||||
| @ -27,7 +28,9 @@ export const ERRORS = { | ||||
|   invalidCode: 'code should be the compiled hex string', | ||||
|   invalidNumber: 'invalid number format', | ||||
|   negativeNumber: 'input number should be positive', | ||||
|   decimalNumber: 'input number should not contain decimals' | ||||
|   decimalNumber: 'input number should not contain decimals', | ||||
|   gasException: 'the transaction will throw an exception with the current values', | ||||
|   gasBlockLimit: 'the transaction execution will exceed the block gas limit' | ||||
| }; | ||||
| 
 | ||||
| export function validateAbi (abi, api) { | ||||
| @ -133,6 +136,25 @@ export function validateName (name) { | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function validatePositiveNumber (number) { | ||||
|   let numberError = null; | ||||
| 
 | ||||
|   try { | ||||
|     const v = new BigNumber(number); | ||||
| 
 | ||||
|     if (v.lt(0)) { | ||||
|       numberError = ERRORS.invalidAmount; | ||||
|     } | ||||
|   } catch (e) { | ||||
|     numberError = ERRORS.invalidAmount; | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     number, | ||||
|     numberError | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function validateUint (value) { | ||||
|   let valueError = null; | ||||
| 
 | ||||
|  | ||||
| @ -15,9 +15,6 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .transactions { | ||||
| } | ||||
| 
 | ||||
| .infonone { | ||||
|   opacity: 0.25; | ||||
| } | ||||
|  | ||||
| @ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| 
 | ||||
| import etherscan from '../../../3rdparty/etherscan'; | ||||
| import etherscan from '~/3rdparty/etherscan'; | ||||
| import { Container, TxList } from '~/ui'; | ||||
| 
 | ||||
| import styles from './transactions.css'; | ||||
|  | ||||
| @ -14,8 +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/>. | ||||
| */ | ||||
| .account { | ||||
| } | ||||
| 
 | ||||
| .btnicon { | ||||
|   width: 24px; | ||||
|  | ||||
| @ -105,7 +105,7 @@ class Account extends Component { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.account }> | ||||
|       <div> | ||||
|         { this.renderDeleteDialog(account) } | ||||
|         { this.renderEditDialog(account) } | ||||
|         { this.renderFundDialog() } | ||||
|  | ||||
| @ -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 BigNumber from 'bignumber.js'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { Link } from 'react-router'; | ||||
| import { isEqual } from 'lodash'; | ||||
| @ -113,15 +114,16 @@ export default class Summary extends Component { | ||||
| 
 | ||||
|   renderOwners () { | ||||
|     const { owners } = this.props; | ||||
|     const ownersValid = (owners || []).filter((owner) => owner.address && new BigNumber(owner.address).gt(0)); | ||||
| 
 | ||||
|     if (!owners || owners.length === 0) { | ||||
|     if (!ownersValid || ownersValid.length === 0) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.owners }> | ||||
|         { | ||||
|           owners.map((owner) => ( | ||||
|           ownersValid.map((owner) => ( | ||||
|             <div key={ owner.address }> | ||||
|               <div | ||||
|                 data-tip | ||||
|  | ||||
| @ -14,8 +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/>. | ||||
| */ | ||||
| .accounts { | ||||
| } | ||||
| 
 | ||||
| .accountTooltip { | ||||
|   top: 13.3em; | ||||
|  | ||||
| @ -82,7 +82,7 @@ class Accounts extends Component { | ||||
| 
 | ||||
|   render () { | ||||
|     return ( | ||||
|       <div className={ styles.accounts }> | ||||
|       <div> | ||||
|         { this.renderNewDialog() } | ||||
|         { this.renderNewWalletDialog() } | ||||
|         { this.renderActionbar() } | ||||
| @ -293,13 +293,17 @@ function mapStateToProps (state) { | ||||
| 
 | ||||
|   const walletsOwners = Object | ||||
|     .keys(walletsInfo) | ||||
|     .map((wallet) => ({ | ||||
|       owners: walletsInfo[wallet].owners.map((owner) => ({ | ||||
|         address: owner, | ||||
|         name: accountsInfo[owner] && accountsInfo[owner].name || owner | ||||
|       })), | ||||
|       address: wallet | ||||
|     })) | ||||
|     .map((wallet) => { | ||||
|       const owners = walletsInfo[wallet].owners || []; | ||||
| 
 | ||||
|       return { | ||||
|         owners: owners.map((owner) => ({ | ||||
|           address: owner, | ||||
|           name: accountsInfo[owner] && accountsInfo[owner].name || owner | ||||
|         })), | ||||
|         address: wallet | ||||
|       }; | ||||
|     }) | ||||
|     .reduce((walletsOwners, wallet) => { | ||||
|       walletsOwners[wallet.address] = wallet.owners; | ||||
|       return walletsOwners; | ||||
|  | ||||
| @ -14,37 +14,37 @@ | ||||
| /* You should have received a copy of the GNU General Public License | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| .address { | ||||
| } | ||||
| 
 | ||||
| .delete .hero { | ||||
|   padding-bottom: 1em; | ||||
| } | ||||
| .delete { | ||||
|   .hero { | ||||
|     padding-bottom: 1em; | ||||
|   } | ||||
| 
 | ||||
| .delete .info { | ||||
|   display: inline-block; | ||||
| } | ||||
|   .info { | ||||
|     display: inline-block; | ||||
|   } | ||||
| 
 | ||||
| .delete .icon { | ||||
|   display: inline-block; | ||||
| } | ||||
|   .icon { | ||||
|     display: inline-block; | ||||
|   } | ||||
| 
 | ||||
| .delete .nameinfo { | ||||
|   display: inline-block; | ||||
|   text-align: left; | ||||
| } | ||||
|   .nameinfo { | ||||
|     display: inline-block; | ||||
|     text-align: left; | ||||
|   } | ||||
| 
 | ||||
| .delete .header { | ||||
|   text-transform: uppercase; | ||||
|   font-size: 1.25em; | ||||
|   padding-bottom: 0.25em; | ||||
| } | ||||
|   .header { | ||||
|     text-transform: uppercase; | ||||
|     font-size: 1.25em; | ||||
|     padding-bottom: 0.25em; | ||||
|   } | ||||
| 
 | ||||
| .delete .address { | ||||
| } | ||||
|   .address { | ||||
|   } | ||||
| 
 | ||||
| .delete .description { | ||||
|   padding-top: 1em; | ||||
|   font-size: 0.75em; | ||||
|   color: #aaa; | ||||
|   .description { | ||||
|     padding-top: 1em; | ||||
|     font-size: 0.75em; | ||||
|     color: #aaa; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,8 +28,6 @@ import Transactions from '../Account/Transactions'; | ||||
| import Delete from './Delete'; | ||||
| import { setVisibleAccounts } from '~/redux/providers/personalActions'; | ||||
| 
 | ||||
| import styles from './address.css'; | ||||
| 
 | ||||
| class Address extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
| @ -85,7 +83,7 @@ class Address extends Component { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.address }> | ||||
|       <div> | ||||
|         { this.renderEditDialog(contact) } | ||||
|         { this.renderActionbar(contact) } | ||||
|         <Delete | ||||
|  | ||||
| @ -14,8 +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/>. | ||||
| */ | ||||
| .addresses { | ||||
| } | ||||
| 
 | ||||
| .list { | ||||
|   display: flex; | ||||
| @ -26,21 +24,21 @@ | ||||
|   flex: 0 1 50%; | ||||
|   width: 50%; | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .address:nth-child(odd)>div { | ||||
|   padding-right: 0.5em !important; | ||||
| } | ||||
|   &:nth-child(odd)>div { | ||||
|     padding-right: 0.5em !important; | ||||
|   } | ||||
| 
 | ||||
| .address:nth-child(even)>div { | ||||
|   padding-left: 0.5em !important; | ||||
|   &:nth-child(even)>div { | ||||
|     padding-left: 0.5em !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .empty { | ||||
|   width: 100%; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .empty div { | ||||
|   color: #aaa; | ||||
|   div { | ||||
|     color: #aaa; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -76,7 +76,7 @@ class Addresses extends Component { | ||||
|     const { searchValues, sortOrder } = this.state; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.addresses }> | ||||
|       <div> | ||||
|         { this.renderActionbar() } | ||||
|         { this.renderAddAddress() } | ||||
|         <Page> | ||||
|  | ||||
| @ -19,7 +19,6 @@ import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import ActionCompareArrows from 'material-ui/svg-icons/action/compare-arrows'; | ||||
| import ActionDashboard from 'material-ui/svg-icons/action/dashboard'; | ||||
| // import CommunicationVpnKey from 'material-ui/svg-icons/communication/vpn-key';
 | ||||
| import HardwareDesktopMac from 'material-ui/svg-icons/hardware/desktop-mac'; | ||||
| import NotificationVpnLock from 'material-ui/svg-icons/notification/vpn-lock'; | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import { IdentityIcon, IdentityName, Input, InputAddress } from '~/ui'; | ||||
| import ShortenedHash from '~/ui/ShortenedHash'; | ||||
| import { txLink } from '../../../../3rdparty/etherscan/links'; | ||||
| import { txLink } from '~/3rdparty/etherscan/links'; | ||||
| 
 | ||||
| import styles from '../../contract.css'; | ||||
| 
 | ||||
|  | ||||
| @ -15,26 +15,26 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .contract { | ||||
| } | ||||
| 
 | ||||
| .events { | ||||
|   width: 100%; | ||||
|   border: none; | ||||
|   border-spacing: 0; | ||||
| } | ||||
| 
 | ||||
| .events tr { | ||||
|   line-height: 32px; | ||||
|   vertical-align: top; | ||||
|   tr { | ||||
|     line-height: 32px; | ||||
|     vertical-align: top; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .event { | ||||
| } | ||||
|   td { | ||||
|     vertical-align: top; | ||||
|     padding: 1em 0.5em; | ||||
| 
 | ||||
| .event td { | ||||
|   vertical-align: top; | ||||
|   padding: 1em 0.5em; | ||||
|     div { | ||||
|       white-space: nowrap; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .txhash { | ||||
| @ -47,10 +47,6 @@ | ||||
|   color: #aaa; | ||||
| } | ||||
| 
 | ||||
| .event td div { | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .mined { | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -124,7 +124,7 @@ class Contract extends Component { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.contract }> | ||||
|       <div> | ||||
|         { this.renderActionbar(account) } | ||||
|         { this.renderDeleteDialog(account) } | ||||
|         { this.renderEditDialog(account) } | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| /* Copyright 2015, 2016 Ethcore (UK) Ltd. | ||||
| /* This file is part of Parity. | ||||
| /* | ||||
| /* Parity is free software: you can redistribute it and/or modify | ||||
| /* it under the terms of the GNU General Public License as published by | ||||
| /* the Free Software Foundation, either version 3 of the License, or | ||||
| /* (at your option) any later version. | ||||
| /* | ||||
| /* Parity is distributed in the hope that it will be useful, | ||||
| /* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| /* GNU General Public License for more details. | ||||
| /* | ||||
| /* You should have received a copy of the GNU General Public License | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| .contracts { | ||||
| } | ||||
| @ -28,8 +28,6 @@ import { setVisibleAccounts } from '~/redux/providers/personalActions'; | ||||
| 
 | ||||
| import List from '../Accounts/List'; | ||||
| 
 | ||||
| import styles from './contracts.css'; | ||||
| 
 | ||||
| class Contracts extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired | ||||
| @ -80,7 +78,7 @@ class Contracts extends Component { | ||||
|     const { searchValues, sortOrder } = this.state; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.contracts }> | ||||
|       <div> | ||||
|         { this.renderActionbar() } | ||||
|         { this.renderAddContract() } | ||||
|         { this.renderAddContract() } | ||||
| @ -159,7 +157,6 @@ class Contracts extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <Actionbar | ||||
|         className={ styles.toolbar } | ||||
|         title='Contracts' | ||||
|         buttons={ buttons } /> | ||||
|     ); | ||||
|  | ||||
| @ -15,9 +15,6 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .layout { | ||||
| } | ||||
| 
 | ||||
| .menu { | ||||
|   display: inline-block; | ||||
| } | ||||
| @ -35,31 +32,24 @@ | ||||
|   padding: 16px 2em !important; | ||||
|   line-height: 24px !important; | ||||
|   width: auto !important; | ||||
| } | ||||
| 
 | ||||
| .tabactive { | ||||
| } | ||||
|   &>div { | ||||
|     height: 24px !important; | ||||
| 
 | ||||
| .tab>div, | ||||
| .tabactive>div { | ||||
|   height: 24px !important; | ||||
| } | ||||
|     &>div { | ||||
|       display: inline-block !important; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| .tab>div>div, | ||||
| .tabactive>div>div { | ||||
|   display: inline-block !important; | ||||
| } | ||||
|   svg { | ||||
|     margin-right: 0.5em; | ||||
|     margin-bottom: 0 !important; | ||||
|   } | ||||
| 
 | ||||
| .tab svg, | ||||
| .tabactive svg { | ||||
|   margin-right: 0.5em; | ||||
|   margin-bottom: 0 !important; | ||||
| } | ||||
| 
 | ||||
| .tab .menu, | ||||
| .tabactive .menu { | ||||
|   vertical-align: top; | ||||
|   display: inline-block; | ||||
|   .menu { | ||||
|     vertical-align: top; | ||||
|     display: inline-block; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .imageIcon { | ||||
| @ -68,6 +58,8 @@ | ||||
|   opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .tabactive .imageIcon { | ||||
|   opacity: 1; | ||||
| .tabactive { | ||||
|   .imageIcon { | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -45,7 +45,7 @@ export default class Settings extends Component { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.layout }> | ||||
|       <div> | ||||
|         <Actionbar title='settings' className={ styles.bar }> | ||||
|           <Tabs className={ styles.tabs } value={ hash }> | ||||
|             { this.renderTab(hash, 'views', <ImageRemoveRedEye />) } | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
| 
 | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import { addressLink } from '../../../../../3rdparty/etherscan/links'; | ||||
| import { addressLink } from '~/3rdparty/etherscan/links'; | ||||
| import styles from './AccountLink.css'; | ||||
| 
 | ||||
| export default class AccountLink extends Component { | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
| 
 | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| 
 | ||||
| import { txLink } from '../../../../3rdparty/etherscan/links'; | ||||
| import { txLink } from '~/3rdparty/etherscan/links'; | ||||
| 
 | ||||
| export default class TxHashLink extends Component { | ||||
| 
 | ||||
|  | ||||
| @ -23,9 +23,6 @@ | ||||
|   width: $embedWidth; | ||||
| } | ||||
| 
 | ||||
| .pending { | ||||
| } | ||||
| 
 | ||||
| .none { | ||||
|   color: #aaa; | ||||
| } | ||||
|  | ||||
| @ -71,7 +71,7 @@ class Embedded extends Component { | ||||
|     const items = pending.sort(this._sortRequests).map(this.renderPending); | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.pending }> | ||||
|       <div> | ||||
|         { items } | ||||
|       </div> | ||||
|     ); | ||||
|  | ||||
| @ -15,12 +15,6 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .request { | ||||
| } | ||||
| 
 | ||||
| .noRequestsMsg { | ||||
|   color: #aaa; | ||||
| } | ||||
| 
 | ||||
| .items { | ||||
| } | ||||
|  | ||||
| @ -98,9 +98,7 @@ class RequestsPage extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <Container title='Pending Requests'> | ||||
|         <div className={ styles.items }> | ||||
|           { items } | ||||
|         </div> | ||||
|         { items } | ||||
|       </Container> | ||||
|     ); | ||||
|   } | ||||
| @ -111,7 +109,6 @@ class RequestsPage extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <RequestPending | ||||
|         className={ styles.request } | ||||
|         onConfirm={ actions.startConfirmRequest } | ||||
|         onReject={ actions.startRejectRequest } | ||||
|         isSending={ isSending || false } | ||||
|  | ||||
| @ -19,12 +19,10 @@ import React, { Component } from 'react'; | ||||
| import { Actionbar } from '~/ui'; | ||||
| import RequestsPage from './containers/RequestsPage'; | ||||
| 
 | ||||
| import styles from './signer.css'; | ||||
| 
 | ||||
| export default class Signer extends Component { | ||||
|   render () { | ||||
|     return ( | ||||
|       <div className={ styles.signer }> | ||||
|       <div> | ||||
|         <Actionbar | ||||
|           title='Trusted Signer' /> | ||||
|         <RequestsPage /> | ||||
|  | ||||
| @ -23,8 +23,6 @@ import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '~/redux/ | ||||
| import Debug from '../../components/Debug'; | ||||
| import Status from '../../components/Status'; | ||||
| 
 | ||||
| import styles from './statusPage.css'; | ||||
| 
 | ||||
| class StatusPage extends Component { | ||||
|   static propTypes = { | ||||
|     nodeStatus: PropTypes.object.isRequired, | ||||
| @ -41,7 +39,7 @@ class StatusPage extends Component { | ||||
| 
 | ||||
|   render () { | ||||
|     return ( | ||||
|       <div className={ styles.body }> | ||||
|       <div> | ||||
|         <Status { ...this.props } /> | ||||
|         <Debug { ...this.props } /> | ||||
|       </div> | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| /* Copyright 2015, 2016 Ethcore (UK) Ltd. | ||||
| /* This file is part of Parity. | ||||
| /* | ||||
| /* Parity is free software: you can redistribute it and/or modify | ||||
| /* it under the terms of the GNU General Public License as published by | ||||
| /* the Free Software Foundation, either version 3 of the License, or | ||||
| /* (at your option) any later version. | ||||
| /* | ||||
| /* Parity is distributed in the hope that it will be useful, | ||||
| /* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| /* GNU General Public License for more details. | ||||
| /* | ||||
| /* You should have received a copy of the GNU General Public License | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| .body { | ||||
| } | ||||
| @ -30,6 +30,10 @@ pub struct AuthorityRoundParams { | ||||
| 	pub step_duration: Uint, | ||||
| 	/// Valid authorities
 | ||||
| 	pub authorities: Vec<Address>, | ||||
| 	/// Starting step. Determined automatically if not specified.
 | ||||
| 	/// To be used for testing only.
 | ||||
| 	#[serde(rename="startStep")] | ||||
| 	pub start_step: Option<Uint>, | ||||
| } | ||||
| 
 | ||||
| /// Authority engine deserialization.
 | ||||
| @ -50,7 +54,8 @@ mod tests { | ||||
| 			"params": { | ||||
| 				"gasLimitBoundDivisor": "0x0400", | ||||
| 				"stepDuration": "0x02", | ||||
| 				"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"] | ||||
| 				"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"], | ||||
| 				"startStep" : 24 | ||||
| 			} | ||||
| 		}"#;
 | ||||
| 
 | ||||
|  | ||||
| @ -27,6 +27,8 @@ heapsize = "0.3" | ||||
| ethcore-ipc = { path = "../ipc/rpc" } | ||||
| semver = "0.2" | ||||
| ethcore-ipc-nano = { path = "../ipc/nano" } | ||||
| ethcore-devtools = { path = "../devtools" } | ||||
| ethkey = { path = "../ethkey" } | ||||
| parking_lot = "0.3" | ||||
| 
 | ||||
| [features] | ||||
|  | ||||
| @ -39,6 +39,9 @@ extern crate rlp; | ||||
| 
 | ||||
| extern crate ethcore_light as light; | ||||
| 
 | ||||
| #[cfg(test)] extern crate ethcore_devtools as devtools; | ||||
| #[cfg(test)] extern crate ethkey; | ||||
| 
 | ||||
| #[macro_use] | ||||
| extern crate log; | ||||
| #[macro_use] | ||||
|  | ||||
| @ -24,8 +24,8 @@ use SyncConfig; | ||||
| fn two_peers() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	net.peer_mut(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.sync(); | ||||
| 	assert!(net.peer(0).chain.block(BlockID::Number(1000)).is_some()); | ||||
| 	assert_eq!(*net.peer(0).chain.blocks.read(), *net.peer(1).chain.blocks.read()); | ||||
| @ -35,7 +35,7 @@ fn two_peers() { | ||||
| fn long_chain() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(1).chain.add_blocks(50000, EachBlockWith::Nothing); | ||||
| 	net.peer(1).chain.add_blocks(50000, EachBlockWith::Nothing); | ||||
| 	net.sync(); | ||||
| 	assert!(net.peer(0).chain.block(BlockID::Number(50000)).is_some()); | ||||
| 	assert_eq!(*net.peer(0).chain.blocks.read(), *net.peer(1).chain.blocks.read()); | ||||
| @ -45,8 +45,8 @@ fn long_chain() { | ||||
| fn status_after_sync() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	net.peer_mut(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.sync(); | ||||
| 	let status = net.peer(0).sync.read().status(); | ||||
| 	assert_eq!(status.state, SyncState::Idle); | ||||
| @ -55,8 +55,8 @@ fn status_after_sync() { | ||||
| #[test] | ||||
| fn takes_few_steps() { | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	let total_steps = net.sync(); | ||||
| 	assert!(total_steps < 20); | ||||
| } | ||||
| @ -67,8 +67,8 @@ fn empty_blocks() { | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	for n in 0..200 { | ||||
| 		let with = if n % 2 == 0 { EachBlockWith::Nothing } else { EachBlockWith::Uncle }; | ||||
| 		net.peer_mut(1).chain.add_blocks(5, with.clone()); | ||||
| 		net.peer_mut(2).chain.add_blocks(5, with); | ||||
| 		net.peer(1).chain.add_blocks(5, with.clone()); | ||||
| 		net.peer(2).chain.add_blocks(5, with); | ||||
| 	} | ||||
| 	net.sync(); | ||||
| 	assert!(net.peer(0).chain.block(BlockID::Number(1000)).is_some()); | ||||
| @ -79,14 +79,14 @@ fn empty_blocks() { | ||||
| fn forked() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	net.peer_mut(0).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(0).chain.add_blocks(10, EachBlockWith::Nothing); //fork
 | ||||
| 	net.peer_mut(1).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); //fork between 1 and 2
 | ||||
| 	net.peer_mut(2).chain.add_blocks(1, EachBlockWith::Nothing); | ||||
| 	net.peer(0).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(30, EachBlockWith::Uncle); | ||||
| 	net.peer(0).chain.add_blocks(10, EachBlockWith::Nothing); //fork
 | ||||
| 	net.peer(1).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); //fork between 1 and 2
 | ||||
| 	net.peer(2).chain.add_blocks(1, EachBlockWith::Nothing); | ||||
| 	// peer 1 has the best chain of 601 blocks
 | ||||
| 	let peer1_chain = net.peer(1).chain.numbers.read().clone(); | ||||
| 	net.sync(); | ||||
| @ -102,12 +102,12 @@ fn forked_with_misbehaving_peer() { | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	// peer 0 is on a totally different chain with higher total difficulty
 | ||||
| 	net.peer_mut(0).chain = TestBlockChainClient::new_with_extra_data(b"fork".to_vec()); | ||||
| 	net.peer_mut(0).chain.add_blocks(50, EachBlockWith::Nothing); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 	net.peer_mut(2).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 	net.peer(0).chain.add_blocks(50, EachBlockWith::Nothing); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 	net.peer(2).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 
 | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 	net.peer_mut(2).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Nothing); | ||||
| 	net.peer(2).chain.add_blocks(20, EachBlockWith::Uncle); | ||||
| 	// peer 1 should sync to peer 2, others should not change
 | ||||
| 	let peer0_chain = net.peer(0).chain.numbers.read().clone(); | ||||
| 	let peer2_chain = net.peer(2).chain.numbers.read().clone(); | ||||
| @ -124,13 +124,13 @@ fn net_hard_fork() { | ||||
| 	ref_client.add_blocks(50, EachBlockWith::Uncle); | ||||
| 	{ | ||||
| 		let mut net = TestNet::new_with_fork(2, Some((50, ref_client.block_hash(BlockID::Number(50)).unwrap()))); | ||||
| 		net.peer_mut(0).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 		net.peer(0).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 		net.sync(); | ||||
| 		assert_eq!(net.peer(1).chain.chain_info().best_block_number, 100); | ||||
| 	} | ||||
| 	{ | ||||
| 		let mut net = TestNet::new_with_fork(2, Some((50, ref_client.block_hash(BlockID::Number(50)).unwrap()))); | ||||
| 		net.peer_mut(0).chain.add_blocks(100, EachBlockWith::Nothing); | ||||
| 		net.peer(0).chain.add_blocks(100, EachBlockWith::Nothing); | ||||
| 		net.sync(); | ||||
| 		assert_eq!(net.peer(1).chain.chain_info().best_block_number, 0); | ||||
| 	} | ||||
| @ -140,8 +140,8 @@ fn net_hard_fork() { | ||||
| fn restart() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(3); | ||||
| 	net.peer_mut(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 	net.peer(2).chain.add_blocks(1000, EachBlockWith::Uncle); | ||||
| 
 | ||||
| 	net.sync(); | ||||
| 
 | ||||
| @ -166,37 +166,37 @@ fn status_empty() { | ||||
| #[test] | ||||
| fn status_packet() { | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(0).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.add_blocks(1, EachBlockWith::Uncle); | ||||
| 	net.peer(0).chain.add_blocks(100, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(1, EachBlockWith::Uncle); | ||||
| 
 | ||||
| 	net.start(); | ||||
| 
 | ||||
| 	net.sync_step_peer(0); | ||||
| 
 | ||||
| 	assert_eq!(1, net.peer(0).queue.len()); | ||||
| 	assert_eq!(0x00, net.peer(0).queue[0].packet_id); | ||||
| 	assert_eq!(1, net.peer(0).queue.read().len()); | ||||
| 	assert_eq!(0x00, net.peer(0).queue.read()[0].packet_id); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn propagate_hashes() { | ||||
| 	let mut net = TestNet::new(6); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.sync(); | ||||
| 
 | ||||
| 	net.peer_mut(0).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(0).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.sync(); | ||||
| 	net.trigger_chain_new_blocks(0); //first event just sets the marker
 | ||||
| 	net.trigger_chain_new_blocks(0); | ||||
| 
 | ||||
| 	// 5 peers with NewHahses, 4 with blocks
 | ||||
| 	assert_eq!(9, net.peer(0).queue.len()); | ||||
| 	assert_eq!(9, net.peer(0).queue.read().len()); | ||||
| 	let mut hashes = 0; | ||||
| 	let mut blocks = 0; | ||||
| 	for i in 0..net.peer(0).queue.len() { | ||||
| 		if net.peer(0).queue[i].packet_id == 0x1 { | ||||
| 	for i in 0..net.peer(0).queue.read().len() { | ||||
| 		if net.peer(0).queue.read()[i].packet_id == 0x1 { | ||||
| 			hashes += 1; | ||||
| 		} | ||||
| 		if net.peer(0).queue[i].packet_id == 0x7 { | ||||
| 		if net.peer(0).queue.read()[i].packet_id == 0x7 { | ||||
| 			blocks += 1; | ||||
| 		} | ||||
| 	} | ||||
| @ -207,24 +207,24 @@ fn propagate_hashes() { | ||||
| #[test] | ||||
| fn propagate_blocks() { | ||||
| 	let mut net = TestNet::new(20); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.sync(); | ||||
| 
 | ||||
| 	net.peer_mut(0).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(0).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.trigger_chain_new_blocks(0); //first event just sets the marker
 | ||||
| 	net.trigger_chain_new_blocks(0); | ||||
| 
 | ||||
| 	assert!(!net.peer(0).queue.is_empty()); | ||||
| 	assert!(!net.peer(0).queue.read().is_empty()); | ||||
| 	// NEW_BLOCK_PACKET
 | ||||
| 	let blocks = net.peer(0).queue.iter().filter(|p| p.packet_id == 0x7).count(); | ||||
| 	let blocks = net.peer(0).queue.read().iter().filter(|p| p.packet_id == 0x7).count(); | ||||
| 	assert!(blocks > 0); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn restart_on_malformed_block() { | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.corrupt_block(6); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.corrupt_block(6); | ||||
| 	net.sync_steps(20); | ||||
| 
 | ||||
| 	assert_eq!(net.peer(0).chain.chain_info().best_block_number, 5); | ||||
| @ -233,8 +233,8 @@ fn restart_on_malformed_block() { | ||||
| #[test] | ||||
| fn restart_on_broken_chain() { | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.corrupt_block_parent(6); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.corrupt_block_parent(6); | ||||
| 	net.sync_steps(20); | ||||
| 
 | ||||
| 	assert_eq!(net.peer(0).chain.chain_info().best_block_number, 5); | ||||
| @ -243,8 +243,8 @@ fn restart_on_broken_chain() { | ||||
| #[test] | ||||
| fn high_td_attach() { | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.corrupt_block_parent(6); | ||||
| 	net.peer(1).chain.add_blocks(10, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.corrupt_block_parent(6); | ||||
| 	net.sync_steps(20); | ||||
| 
 | ||||
| 	assert_eq!(net.peer(0).chain.chain_info().best_block_number, 5); | ||||
| @ -255,8 +255,8 @@ fn high_td_attach() { | ||||
| fn disconnect_on_unrelated_chain() { | ||||
| 	::env_logger::init().ok(); | ||||
| 	let mut net = TestNet::new(2); | ||||
| 	net.peer_mut(0).chain.add_blocks(200, EachBlockWith::Uncle); | ||||
| 	net.peer_mut(1).chain.add_blocks(100, EachBlockWith::Nothing); | ||||
| 	net.peer(0).chain.add_blocks(200, EachBlockWith::Uncle); | ||||
| 	net.peer(1).chain.add_blocks(100, EachBlockWith::Nothing); | ||||
| 	net.sync(); | ||||
| 	assert_eq!(net.disconnect_events, vec![(0, 0)]); | ||||
| } | ||||
|  | ||||
							
								
								
									
										78
									
								
								sync/src/tests/consensus.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								sync/src/tests/consensus.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| // Copyright 2015, 2016 Ethcore (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| use util::*; | ||||
| use ethcore::client::BlockChainClient; | ||||
| use ethcore::spec::Spec; | ||||
| use ethcore::miner::MinerService; | ||||
| use ethcore::transaction::*; | ||||
| use ethcore::account_provider::AccountProvider; | ||||
| use ethkey::KeyPair; | ||||
| use super::helpers::*; | ||||
| use SyncConfig; | ||||
| 
 | ||||
| #[test] | ||||
| fn test_authority_round() { | ||||
| 	::env_logger::init().ok(); | ||||
| 
 | ||||
| 	let s1 = KeyPair::from_secret("1".sha3()).unwrap(); | ||||
| 	let s2 = KeyPair::from_secret("0".sha3()).unwrap(); | ||||
| 	let spec_factory = || { | ||||
| 		let spec = Spec::new_test_round(); | ||||
| 		let account_provider = AccountProvider::transient_provider(); | ||||
| 		account_provider.insert_account(s1.secret().clone(), "").unwrap(); | ||||
| 		account_provider.insert_account(s2.secret().clone(), "").unwrap(); | ||||
| 		spec.engine.register_account_provider(Arc::new(account_provider)); | ||||
| 		spec | ||||
| 	}; | ||||
| 	let mut net = TestNet::new_with_spec(2, SyncConfig::default(), spec_factory); | ||||
| 	let mut net = &mut *net; | ||||
| 	// Push transaction to both clients. Only one of them gets lucky to mine a block.
 | ||||
| 	net.peer(0).chain.miner().set_author(s1.address()); | ||||
| 	net.peer(0).chain.engine().set_signer(s1.address(), "".to_owned()); | ||||
| 	net.peer(1).chain.miner().set_author(s2.address()); | ||||
| 	net.peer(1).chain.engine().set_signer(s2.address(), "".to_owned()); | ||||
| 	let tx1 = Transaction { | ||||
| 		nonce: 0.into(), | ||||
| 		gas_price: 0.into(), | ||||
| 		gas: 21000.into(), | ||||
| 		action: Action::Call(Address::default()), | ||||
| 		value: 0.into(), | ||||
| 		data: Vec::new(), | ||||
| 	}.sign(s1.secret(), None); | ||||
| 	// exhange statuses
 | ||||
| 	net.sync_steps(5); | ||||
| 	net.peer(0).chain.miner().import_own_transaction(&net.peer(0).chain, tx1).unwrap(); | ||||
| 	net.sync(); | ||||
| 	assert_eq!(net.peer(0).chain.chain_info().best_block_number, 1); | ||||
| 	assert_eq!(net.peer(1).chain.chain_info().best_block_number, 1); | ||||
| 
 | ||||
| 	let tx2 = Transaction { | ||||
| 		nonce: 0.into(), | ||||
| 		gas_price: 0.into(), | ||||
| 		gas: 21000.into(), | ||||
| 		action: Action::Call(Address::default()), | ||||
| 		value: 0.into(), | ||||
| 		data: Vec::new(), | ||||
| 	}.sign(s2.secret(), None); | ||||
| 	net.peer(1).chain.miner().import_own_transaction(&net.peer(1).chain, tx2).unwrap(); | ||||
| 	net.peer(1).chain.engine().step(); | ||||
| 	net.peer(1).chain.miner().update_sealing(&net.peer(1).chain); | ||||
| 	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); | ||||
| } | ||||
| 
 | ||||
| @ -17,16 +17,33 @@ | ||||
| use util::*; | ||||
| use network::*; | ||||
| use tests::snapshot::*; | ||||
| use ethcore::client::{TestBlockChainClient, BlockChainClient}; | ||||
| use ethcore::client::{TestBlockChainClient, BlockChainClient, Client as EthcoreClient, ClientConfig, ChainNotify}; | ||||
| use ethcore::header::BlockNumber; | ||||
| use ethcore::snapshot::SnapshotService; | ||||
| use ethcore::spec::Spec; | ||||
| use ethcore::miner::Miner; | ||||
| use ethcore::db::NUM_COLUMNS; | ||||
| use sync_io::SyncIo; | ||||
| use io::IoChannel; | ||||
| use api::WARP_SYNC_PROTOCOL_ID; | ||||
| use chain::ChainSync; | ||||
| use ::SyncConfig; | ||||
| use devtools::{self, GuardedTempResult}; | ||||
| 
 | ||||
| pub struct TestIo<'p> { | ||||
| 	pub chain: &'p mut TestBlockChainClient, | ||||
| pub trait FlushingBlockChainClient: BlockChainClient { | ||||
| 	fn flush(&self) {} | ||||
| } | ||||
| 
 | ||||
| impl FlushingBlockChainClient for EthcoreClient { | ||||
| 	fn flush(&self) { | ||||
| 		self.flush_queue(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl FlushingBlockChainClient for TestBlockChainClient {} | ||||
| 
 | ||||
| pub struct TestIo<'p, C> where C: FlushingBlockChainClient, C: 'p { | ||||
| 	pub chain: &'p C, | ||||
| 	pub snapshot_service: &'p TestSnapshotService, | ||||
| 	pub queue: &'p mut VecDeque<TestPacket>, | ||||
| 	pub sender: Option<PeerId>, | ||||
| @ -34,8 +51,8 @@ pub struct TestIo<'p> { | ||||
| 	overlay: RwLock<HashMap<BlockNumber, Bytes>>, | ||||
| } | ||||
| 
 | ||||
| impl<'p> TestIo<'p> { | ||||
| 	pub fn new(chain: &'p mut TestBlockChainClient, ss: &'p TestSnapshotService, queue: &'p mut VecDeque<TestPacket>, sender: Option<PeerId>) -> TestIo<'p> { | ||||
| impl<'p, C> TestIo<'p, C> where C: FlushingBlockChainClient, C: 'p { | ||||
| 	pub fn new(chain: &'p C, ss: &'p TestSnapshotService, queue: &'p mut VecDeque<TestPacket>, sender: Option<PeerId>) -> TestIo<'p, C> { | ||||
| 		TestIo { | ||||
| 			chain: chain, | ||||
| 			snapshot_service: ss, | ||||
| @ -47,7 +64,7 @@ impl<'p> TestIo<'p> { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl<'p> SyncIo for TestIo<'p> { | ||||
| impl<'p, C> SyncIo for TestIo<'p, C> where C: FlushingBlockChainClient, C: 'p { | ||||
| 	fn disable_peer(&mut self, peer_id: PeerId) { | ||||
| 		self.disconnect_peer(peer_id); | ||||
| 	} | ||||
| @ -99,7 +116,7 @@ impl<'p> SyncIo for TestIo<'p> { | ||||
| 	} | ||||
| 
 | ||||
| 	fn protocol_version(&self, protocol: &ProtocolId, peer_id: PeerId) -> u8 { | ||||
| 		if protocol == &WARP_SYNC_PROTOCOL_ID { 1 } else { self.eth_protocol_version(peer_id) } | ||||
| 		if protocol == &WARP_SYNC_PROTOCOL_ID { 2 } else { self.eth_protocol_version(peer_id) } | ||||
| 	} | ||||
| 
 | ||||
| 	fn chain_overlay(&self) -> &RwLock<HashMap<BlockNumber, Bytes>> { | ||||
| @ -113,31 +130,31 @@ pub struct TestPacket { | ||||
| 	pub recipient: PeerId, | ||||
| } | ||||
| 
 | ||||
| pub struct TestPeer { | ||||
| 	pub chain: TestBlockChainClient, | ||||
| pub struct TestPeer<C> where C: FlushingBlockChainClient { | ||||
| 	pub chain: C, | ||||
| 	pub snapshot_service: Arc<TestSnapshotService>, | ||||
| 	pub sync: RwLock<ChainSync>, | ||||
| 	pub queue: VecDeque<TestPacket>, | ||||
| 	pub queue: RwLock<VecDeque<TestPacket>>, | ||||
| } | ||||
| 
 | ||||
| pub struct TestNet { | ||||
| 	pub peers: Vec<TestPeer>, | ||||
| pub struct TestNet<C> where C: FlushingBlockChainClient { | ||||
| 	pub peers: Vec<Arc<TestPeer<C>>>, | ||||
| 	pub started: bool, | ||||
| 	pub disconnect_events: Vec<(PeerId, PeerId)>, //disconnected (initiated by, to)
 | ||||
| } | ||||
| 
 | ||||
| impl TestNet { | ||||
| 	pub fn new(n: usize) -> TestNet { | ||||
| impl TestNet<TestBlockChainClient> { | ||||
| 	pub fn new(n: usize) -> TestNet<TestBlockChainClient> { | ||||
| 		Self::new_with_config(n, SyncConfig::default()) | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn new_with_fork(n: usize, fork: Option<(BlockNumber, H256)>) -> TestNet { | ||||
| 	pub fn new_with_fork(n: usize, fork: Option<(BlockNumber, H256)>) -> TestNet<TestBlockChainClient> { | ||||
| 		let mut config = SyncConfig::default(); | ||||
| 		config.fork_block = fork; | ||||
| 		Self::new_with_config(n, config) | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn new_with_config(n: usize, config: SyncConfig) -> TestNet { | ||||
| 	pub fn new_with_config(n: usize, config: SyncConfig) -> TestNet<TestBlockChainClient> { | ||||
| 		let mut net = TestNet { | ||||
| 			peers: Vec::new(), | ||||
| 			started: false, | ||||
| @ -147,31 +164,77 @@ impl TestNet { | ||||
| 			let chain = TestBlockChainClient::new(); | ||||
| 			let ss = Arc::new(TestSnapshotService::new()); | ||||
| 			let sync = ChainSync::new(config.clone(), &chain); | ||||
| 			net.peers.push(TestPeer { | ||||
| 			net.peers.push(Arc::new(TestPeer { | ||||
| 				sync: RwLock::new(sync), | ||||
| 				snapshot_service: ss, | ||||
| 				chain: chain, | ||||
| 				queue: VecDeque::new(), | ||||
| 			}); | ||||
| 				queue: RwLock::new(VecDeque::new()), | ||||
| 			})); | ||||
| 		} | ||||
| 		net | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 	pub fn peer(&self, i: usize) -> &TestPeer { | ||||
| impl TestNet<EthcoreClient> { | ||||
| 	pub fn new_with_spec<F>(n: usize, config: SyncConfig, spec_factory: F) -> GuardedTempResult<TestNet<EthcoreClient>> | ||||
| 		where F: Fn() -> Spec | ||||
| 	{ | ||||
| 		let mut net = TestNet { | ||||
| 			peers: Vec::new(), | ||||
| 			started: false, | ||||
| 			disconnect_events: Vec::new(), | ||||
| 		}; | ||||
| 		let dir = devtools::RandomTempPath::new(); | ||||
| 		for _ in 0..n { | ||||
| 			let mut client_dir = dir.as_path().clone(); | ||||
| 			client_dir.push(devtools::random_filename()); | ||||
| 
 | ||||
| 			let db_config = DatabaseConfig::with_columns(NUM_COLUMNS); | ||||
| 
 | ||||
| 			let spec = spec_factory(); | ||||
| 			let client = Arc::try_unwrap(EthcoreClient::new( | ||||
| 				ClientConfig::default(), | ||||
| 				&spec, | ||||
| 				client_dir.as_path(), | ||||
| 				Arc::new(Miner::with_spec(&spec)), | ||||
| 				IoChannel::disconnected(), | ||||
| 				&db_config | ||||
| 			).unwrap()).ok().unwrap(); | ||||
| 
 | ||||
| 			let ss = Arc::new(TestSnapshotService::new()); | ||||
| 			let sync = ChainSync::new(config.clone(), &client); | ||||
| 			let peer = Arc::new(TestPeer { | ||||
| 				sync: RwLock::new(sync), | ||||
| 				snapshot_service: ss, | ||||
| 				chain: client, | ||||
| 				queue: RwLock::new(VecDeque::new()), | ||||
| 			}); | ||||
| 			peer.chain.add_notify(peer.clone()); | ||||
| 			net.peers.push(peer); | ||||
| 		} | ||||
| 		GuardedTempResult::<TestNet<EthcoreClient>> { | ||||
| 			_temp: dir, | ||||
| 			result: Some(net) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl<C> TestNet<C> where C: FlushingBlockChainClient { | ||||
| 	pub fn peer(&self, i: usize) -> &TestPeer<C> { | ||||
| 		&self.peers[i] | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn peer_mut(&mut self, i: usize) -> &mut TestPeer { | ||||
| 		&mut self.peers[i] | ||||
| 	pub fn peer_mut(&mut self, i: usize) -> &mut TestPeer<C> { | ||||
| 		Arc::get_mut(&mut self.peers[i]).unwrap() | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn start(&mut self) { | ||||
| 		for peer in 0..self.peers.len() { | ||||
| 			for client in 0..self.peers.len() { | ||||
| 				if peer != client { | ||||
| 					let mut p = &mut self.peers[peer]; | ||||
| 					let p = &self.peers[peer]; | ||||
| 					p.sync.write().update_targets(&p.chain); | ||||
| 					p.sync.write().on_peer_connected(&mut TestIo::new(&mut p.chain, &p.snapshot_service, &mut p.queue, Some(client as PeerId)), client as PeerId); | ||||
| 					p.sync.write().on_peer_connected(&mut TestIo::new(&p.chain, &p.snapshot_service, &mut p.queue.write(), Some(client as PeerId)), client as PeerId); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @ -179,18 +242,20 @@ impl TestNet { | ||||
| 
 | ||||
| 	pub fn sync_step(&mut self) { | ||||
| 		for peer in 0..self.peers.len() { | ||||
| 			if let Some(packet) = self.peers[peer].queue.pop_front() { | ||||
| 			let packet = self.peers[peer].queue.write().pop_front(); | ||||
| 			if let Some(packet) = packet { | ||||
| 				let disconnecting = { | ||||
| 					let mut p = &mut self.peers[packet.recipient]; | ||||
| 					let p = &self.peers[packet.recipient]; | ||||
| 					let mut queue = p.queue.write(); | ||||
| 					trace!("--- {} -> {} ---", peer, packet.recipient); | ||||
| 					let to_disconnect = { | ||||
| 						let mut io = TestIo::new(&mut p.chain, &p.snapshot_service, &mut p.queue, Some(peer as PeerId)); | ||||
| 						let mut io = TestIo::new(&p.chain, &p.snapshot_service, &mut queue, Some(peer as PeerId)); | ||||
| 						ChainSync::dispatch_packet(&p.sync, &mut io, peer as PeerId, packet.packet_id, &packet.data); | ||||
| 						io.to_disconnect | ||||
| 					}; | ||||
| 					for d in &to_disconnect { | ||||
| 						// notify this that disconnecting peers are disconnecting
 | ||||
| 						let mut io = TestIo::new(&mut p.chain, &p.snapshot_service, &mut p.queue, Some(*d)); | ||||
| 						let mut io = TestIo::new(&p.chain, &p.snapshot_service, &mut queue, Some(*d)); | ||||
| 						p.sync.write().on_peer_aborting(&mut io, *d); | ||||
| 						self.disconnect_events.push((peer, *d)); | ||||
| 					} | ||||
| @ -198,8 +263,9 @@ impl TestNet { | ||||
| 				}; | ||||
| 				for d in &disconnecting { | ||||
| 					// notify other peers that this peer is disconnecting
 | ||||
| 					let mut p = &mut self.peers[*d]; | ||||
| 					let mut io = TestIo::new(&mut p.chain, &p.snapshot_service, &mut p.queue, Some(peer as PeerId)); | ||||
| 					let p = &self.peers[*d]; | ||||
| 					let mut queue = p.queue.write(); | ||||
| 					let mut io = TestIo::new(&p.chain, &p.snapshot_service, &mut queue, Some(peer as PeerId)); | ||||
| 					p.sync.write().on_peer_aborting(&mut io, peer as PeerId); | ||||
| 				} | ||||
| 			} | ||||
| @ -209,13 +275,17 @@ impl TestNet { | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn sync_step_peer(&mut self, peer_num: usize) { | ||||
| 		let mut peer = self.peer_mut(peer_num); | ||||
| 		peer.sync.write().maintain_sync(&mut TestIo::new(&mut peer.chain, &peer.snapshot_service, &mut peer.queue, None)); | ||||
| 		let peer = self.peer(peer_num); | ||||
| 		peer.chain.flush(); | ||||
| 		let mut queue = peer.queue.write(); | ||||
| 		peer.sync.write().maintain_peers(&mut TestIo::new(&peer.chain, &peer.snapshot_service, &mut queue, None)); | ||||
| 		peer.sync.write().maintain_sync(&mut TestIo::new(&peer.chain, &peer.snapshot_service, &mut queue, None)); | ||||
| 		peer.sync.write().propagate_new_transactions(&mut TestIo::new(&peer.chain, &peer.snapshot_service, &mut queue, None)); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn restart_peer(&mut self, i: usize) { | ||||
| 		let peer = self.peer_mut(i); | ||||
| 		peer.sync.write().restart(&mut TestIo::new(&mut peer.chain, &peer.snapshot_service, &mut peer.queue, None)); | ||||
| 		let peer = self.peer(i); | ||||
| 		peer.sync.write().restart(&mut TestIo::new(&peer.chain, &peer.snapshot_service, &mut peer.queue.write(), None)); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn sync(&mut self) -> u32 { | ||||
| @ -239,11 +309,38 @@ impl TestNet { | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn done(&self) -> bool { | ||||
| 		self.peers.iter().all(|p| p.queue.is_empty()) | ||||
| 		self.peers.iter().all(|p| p.queue.read().is_empty()) | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn trigger_chain_new_blocks(&mut self, peer_id: usize) { | ||||
| 		let mut peer = self.peer_mut(peer_id); | ||||
| 		peer.sync.write().chain_new_blocks(&mut TestIo::new(&mut peer.chain, &peer.snapshot_service, &mut peer.queue, None), &[], &[], &[], &[], &[]); | ||||
| 		let peer = self.peer(peer_id); | ||||
| 		let mut queue = peer.queue.write(); | ||||
| 		peer.sync.write().chain_new_blocks(&mut TestIo::new(&peer.chain, &peer.snapshot_service, &mut queue, None), &[], &[], &[], &[], &[]); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl ChainNotify for TestPeer<EthcoreClient> { | ||||
| 	fn new_blocks(&self, | ||||
| 		imported: Vec<H256>, | ||||
| 		invalid: Vec<H256>, | ||||
| 		enacted: Vec<H256>, | ||||
| 		retracted: Vec<H256>, | ||||
| 		sealed: Vec<H256>, | ||||
| 		_duration: u64) | ||||
| 	{ | ||||
| 		let mut queue = self.queue.write(); | ||||
| 		let mut io = TestIo::new(&self.chain, &self.snapshot_service, &mut queue, None); | ||||
| 		self.sync.write().chain_new_blocks( | ||||
| 			&mut io, | ||||
| 			&imported, | ||||
| 			&invalid, | ||||
| 			&enacted, | ||||
| 			&retracted, | ||||
| 			&sealed); | ||||
| 	} | ||||
| 
 | ||||
| 	fn start(&self) {} | ||||
| 
 | ||||
| 	fn stop(&self) {} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -17,6 +17,7 @@ | ||||
| pub mod helpers; | ||||
| pub mod snapshot; | ||||
| mod chain; | ||||
| mod consensus; | ||||
| 
 | ||||
| #[cfg(feature = "ipc")] | ||||
| mod rpc; | ||||
|  | ||||
| @ -129,7 +129,7 @@ fn snapshot_sync() { | ||||
| 	let snapshot_service = Arc::new(TestSnapshotService::new_with_snapshot(16, H256::new(), 500000)); | ||||
| 	for i in 0..4 { | ||||
| 		net.peer_mut(i).snapshot_service = snapshot_service.clone(); | ||||
| 		net.peer_mut(i).chain.add_blocks(1, EachBlockWith::Nothing); | ||||
| 		net.peer(i).chain.add_blocks(1, EachBlockWith::Nothing); | ||||
| 	} | ||||
| 	net.sync_steps(50); | ||||
| 	assert_eq!(net.peer(4).snapshot_service.state_restoration_chunks.lock().len(), net.peer(0).snapshot_service.manifest.as_ref().unwrap().state_hashes.len()); | ||||
|  | ||||
| @ -394,7 +394,7 @@ impl<Message> IoChannel<Message> where Message: Send + Clone + Sync + 'static { | ||||
| /// 'Message' is a notification message type
 | ||||
| pub struct IoService<Message> where Message: Send + Sync + Clone + 'static { | ||||
| 	panic_handler: Arc<PanicHandler>, | ||||
| 	thread: Option<JoinHandle<()>>, | ||||
| 	thread: Mutex<Option<JoinHandle<()>>>, | ||||
| 	host_channel: Mutex<Sender<IoMessage<Message>>>, | ||||
| 	handlers: Arc<RwLock<Slab<Arc<IoHandler<Message>>, HandlerId>>>, | ||||
| } | ||||
| @ -424,12 +424,26 @@ impl<Message> IoService<Message> where Message: Send + Sync + Clone + 'static { | ||||
| 		}); | ||||
| 		Ok(IoService { | ||||
| 			panic_handler: panic_handler, | ||||
| 			thread: Some(thread), | ||||
| 			thread: Mutex::new(Some(thread)), | ||||
| 			host_channel: Mutex::new(channel), | ||||
| 			handlers: handlers, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn stop(&self) { | ||||
| 		trace!(target: "shutdown", "[IoService] Closing..."); | ||||
| 		// Clear handlers so that shared pointers are not stuck on stack
 | ||||
| 		// in Channel::send_sync
 | ||||
| 		self.handlers.write().clear(); | ||||
| 		self.host_channel.lock().send(IoMessage::Shutdown).unwrap_or_else(|e| warn!("Error on IO service shutdown: {:?}", e)); | ||||
| 		if let Some(thread) = self.thread.lock().take() { | ||||
| 			thread.join().unwrap_or_else(|e| { | ||||
| 				debug!(target: "shutdown", "Error joining IO service event loop thread: {:?}", e); | ||||
| 			}); | ||||
| 		} | ||||
| 		trace!(target: "shutdown", "[IoService] Closed."); | ||||
| 	} | ||||
| 
 | ||||
| 	/// Regiter an IO handler with the event loop.
 | ||||
| 	pub fn register_handler(&self, handler: Arc<IoHandler<Message>+Send>) -> Result<(), IoError> { | ||||
| 		try!(self.host_channel.lock().send(IoMessage::AddHandler { | ||||
| @ -452,17 +466,7 @@ impl<Message> IoService<Message> where Message: Send + Sync + Clone + 'static { | ||||
| 
 | ||||
| impl<Message> Drop for IoService<Message> where Message: Send + Sync + Clone { | ||||
| 	fn drop(&mut self) { | ||||
| 		trace!(target: "shutdown", "[IoService] Closing..."); | ||||
| 		// Clear handlers so that shared pointers are not stuck on stack
 | ||||
| 		// in Channel::send_sync
 | ||||
| 		self.handlers.write().clear(); | ||||
| 		self.host_channel.lock().send(IoMessage::Shutdown).unwrap_or_else(|e| warn!("Error on IO service shutdown: {:?}", e)); | ||||
| 		if let Some(thread) = self.thread.take() { | ||||
| 			thread.join().unwrap_or_else(|e| { | ||||
| 				debug!(target: "shutdown", "Error joining IO service event loop thread: {:?}", e); | ||||
| 			}); | ||||
| 		} | ||||
| 		trace!(target: "shutdown", "[IoService] Closed."); | ||||
| 		self.stop() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user