Merge branch 'master' into lightserv

This commit is contained in:
Robert Habermeier 2016-12-09 21:37:55 +01:00
commit 503b126c93
88 changed files with 1606 additions and 736 deletions

View File

@ -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
View File

@ -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)",
]

View File

@ -4,7 +4,8 @@
"AuthorityRound": {
"params": {
"gasLimitBoundDivisor": "0x0400",
"stepDuration": "1",
"stepDuration": 1,
"startStep": 2,
"authorities" : [
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e",
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1"

View File

@ -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());
}
}
}

View File

@ -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.
}

View File

@ -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>;

View File

@ -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
}

View File

@ -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) {}
}

View File

@ -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);

View File

@ -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") }
}

View File

@ -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>",

View File

@ -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}`;
};

View File

@ -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);
}

View File

@ -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) => {

View File

@ -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

View 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;
}

View File

@ -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
})]
};

View File

@ -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 = {

View File

@ -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';

View File

@ -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>
)

View File

@ -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;

View File

@ -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 = {

View File

@ -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';

View File

@ -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';

View File

@ -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 = {

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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';

View File

@ -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';

View File

@ -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;
}

View File

@ -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 = () => {

View File

@ -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;
}

View 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);
}
}

View File

@ -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';

View 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);
});
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -15,9 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.transactions {
}
.infonone {
opacity: 0.25;
}

View File

@ -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';

View File

@ -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;

View File

@ -105,7 +105,7 @@ class Account extends Component {
}
return (
<div className={ styles.account }>
<div>
{ this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) }
{ this.renderFundDialog() }

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -76,7 +76,7 @@ class Addresses extends Component {
const { searchValues, sortOrder } = this.state;
return (
<div className={ styles.addresses }>
<div>
{ this.renderActionbar() }
{ this.renderAddAddress() }
<Page>

View File

@ -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';

View File

@ -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';

View File

@ -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 {
}

View File

@ -124,7 +124,7 @@ class Contract extends Component {
}
return (
<div className={ styles.contract }>
<div>
{ this.renderActionbar(account) }
{ this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) }

View File

@ -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 {
}

View File

@ -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 } />
);

View File

@ -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;
}
}

View File

@ -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 />) }

View File

@ -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 {

View File

@ -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 {

View File

@ -23,9 +23,6 @@
width: $embedWidth;
}
.pending {
}
.none {
color: #aaa;
}

View File

@ -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>
);

View File

@ -15,12 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.request {
}
.noRequestsMsg {
color: #aaa;
}
.items {
}

View File

@ -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 }

View File

@ -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 />

View File

@ -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>

View File

@ -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 {
}

View File

@ -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
}
}"#;

View File

@ -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]

View File

@ -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]

View File

@ -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)]);
}

View 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);
}

View File

@ -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) {}
}

View File

@ -17,6 +17,7 @@
pub mod helpers;
pub mod snapshot;
mod chain;
mod consensus;
#[cfg(feature = "ipc")]
mod rpc;

View File

@ -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());

View File

@ -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()
}
}