Merge branch 'cht-td' into lightrpc

This commit is contained in:
Robert Habermeier 2017-02-04 17:14:47 +01:00
commit 484b93abdc
92 changed files with 1383 additions and 776 deletions

114
Cargo.lock generated
View File

@ -73,6 +73,11 @@ name = "ansi_term"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "antidote"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "app_dirs"
version = "1.1.1"
@ -285,6 +290,16 @@ name = "dtoa"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "dtoa"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "elastic-array"
version = "0.6.0"
@ -515,6 +530,7 @@ dependencies = [
"ethcore-network 1.6.0",
"ethcore-util 1.6.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0",
@ -650,7 +666,7 @@ dependencies = [
"ethcore-bloom-journal 0.1.0",
"ethcore-devtools 1.6.0",
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -715,7 +731,7 @@ dependencies = [
"ethcore-util 1.6.0",
"ethcrypto 0.1.0",
"ethkey 0.2.0",
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -779,7 +795,8 @@ dependencies = [
"futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -896,6 +913,35 @@ dependencies = [
"vecio 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hyper"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hyper-native-tls"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "idna"
version = "0.1.0"
@ -941,14 +987,22 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.4.13"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "itoa"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonrpc-core"
version = "5.0.0"
@ -1509,7 +1563,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#cb8dc7c61dd7976f062863182a1c6d77ba319a36"
source = "git+https://github.com/ethcore/js-precompiled.git#416d00db677b8219f7548bb4dfa2f25c4b19f36e"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1707,15 +1761,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "reqwest"
version = "0.2.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1901,6 +1955,11 @@ name = "serde"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_codegen"
version = "0.8.19"
@ -1933,11 +1992,24 @@ dependencies = [
]
[[package]]
name = "serde_urlencoded"
version = "0.3.0"
name = "serde_json"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
"dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_urlencoded"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -2397,6 +2469,7 @@ dependencies = [
"checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a"
"checksum aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "67077478f0a03952bed2e6786338d400d40c25e9836e08ad50af96607317fd03"
"checksum ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1f46cd5b1d660c938e3f92dfe7a73d832b3281479363dd0cd9c1c2fbf60f7962"
"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
@ -2425,6 +2498,8 @@ dependencies = [
"checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf"
"checksum docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4cc0acb4ce0828c6a5a11d47baa432fe885881c27428c3a4e473e454ffe57a76"
"checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d"
"checksum dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5edd69c67b2f8e0911629b7e6b8a34cb3956613cd7c6e6414966dee349c2db4f"
"checksum either 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2b503c86dad62aaf414ecf2b8c527439abedb3f8d812537f0b12bfd6f32a91"
"checksum elastic-array 0.6.0 (git+https://github.com/ethcore/elastic-array)" = "<none>"
"checksum env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aba65b63ffcc17ffacd6cf5aa843da7c5a25e3bd4bbe0b7def8b214e411250e5"
"checksum eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)" = "<none>"
@ -2441,12 +2516,15 @@ dependencies = [
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae"
"checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>"
"checksum hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "220407e5a263f110ec30a071787c9535918fdfc97def5680c90013c3f30c38c1"
"checksum hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)" = "bcb3fc65554155980167fb821d05c7c66177f92464976c0b676a19d9e03387a7"
"checksum hyper-native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "afe68f772f0497a7205e751626bb8e1718568b58534b6108c73a74ef80483409"
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
"checksum igd 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c8c12b1795b8b168f577c45fa10379b3814dcb11b7ab702406001f0d63f40484"
"checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c"
"checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76"
"checksum itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d95557e7ba6b71377b0f2c3b3ae96c53f1b75a926a6901a500f557a370af730a"
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
"checksum itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91fd9dc2c587067de817fec4ad355e3818c3d893a78cab32a0a474c7a15bb8d5"
"checksum jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
@ -2521,7 +2599,7 @@ dependencies = [
"checksum rayon 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "655df67c314c30fa3055a365eae276eb88aa4f3413a352a1ab32c1320eda41ea"
"checksum regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)" = "b4329b8928a284580a1c63ec9d846b12f6d3472317243ff7077aff11f23f2b29"
"checksum regex-syntax 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "841591b1e05609a643e3b4d0045fce04f701daba7151ddcd3ad47b080693d5a9"
"checksum reqwest 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83186fee0d4dbeb95e610b77b05b05cf5b31703dd375222acb74c3dff4be957c"
"checksum reqwest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bef9ed8fdfcc30947d6b774938dc0c3f369a474efe440df2c7f278180b2d2e6"
"checksum rocksdb 0.4.5 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>"
"checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>"
"checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "<none>"
@ -2540,10 +2618,12 @@ dependencies = [
"checksum semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae2ff60ecdb19c255841c066cbfa5f8c2a4ada1eb3ae47c77ab6667128da71f5"
"checksum semver-parser 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e88e43a5a74dd2a11707f9c21dfd4a423c66bd871df813227bb0a3e78f3a1ae9"
"checksum serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "58a19c0871c298847e6b68318484685cd51fa5478c0c905095647540031356e5"
"checksum serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4d8f810025e9d09c4eaa49c16eaf878f34a947889e878cd7d3b5bef3197cc119"
"checksum serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "ce29a6ae259579707650ec292199b5fed2c0b8e2a4bdc994452d24d1bcf2242a"
"checksum serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "59933a62554548c690d2673c5164f0c4a46be7c5731edfd94b0ecb1048940732"
"checksum serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7d3c184d35801fb8b32b46a7d58d57dbcc150b0eb2b46a1eb79645e8ecfd5b"
"checksum serde_urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "53d4ebaa8d1d4f90d1b63dfca81ccd98ac20e1e479dbae393cbaf60f6fecd8d8"
"checksum serde_json 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fea48f4d4df4e620e3c81fd2bf28c93dd0d266361a76bac4f254b71f0e13f3cd"
"checksum serde_urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81f15da4b9780e1524697f73b09076b6e42298ef673bead9ca8f848b334ef84"
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
"checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d"
"checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd"

View File

@ -22,6 +22,7 @@ time = "0.1"
smallvec = "0.3.1"
futures = "0.1"
rand = "0.3"
itertools = "0.5"
[features]
default = []

View File

@ -12,10 +12,130 @@
// GNU General Public License for more details.
//! Canonical hash trie definitions and helper functions.
//!
//! Each CHT is a trie mapping block numbers to canonical hashes and total difficulty.
//! One is generated for every `SIZE` blocks, allowing us to discard those blocks in
//! favor the the trie root. When the "ancient" blocks need to be accessed, we simply
//! request an inclusion proof of a specific block number against the trie with the
//! root has. A correct proof implies that the claimed block is identical to the one
//! we discarded.
use ethcore::ids::BlockId;
use util::{Bytes, H256, U256, HashDB, MemoryDB};
use util::trie::{self, TrieMut, TrieDBMut, Trie, TrieDB, Recorder};
use rlp::{Stream, RlpStream};
// encode a key.
macro_rules! key {
($num: expr) => { ::rlp::encode(&$num) }
}
macro_rules! val {
($hash: expr, $td: expr) => {{
let mut stream = RlpStream::new_list(2);
stream.append(&$hash).append(&$td);
stream.drain()
}}
}
/// The size of each CHT.
pub const SIZE: u64 = 2048;
/// A canonical hash trie. This is generic over any database it can query.
/// See module docs for more details.
#[derive(Debug, Clone)]
pub struct CHT<DB: HashDB> {
db: DB,
root: H256, // the root of this CHT.
number: u64,
}
impl<DB: HashDB> CHT<DB> {
/// Query the root of the CHT.
pub fn root(&self) -> H256 { self.root }
/// Query the number of the CHT.
pub fn number(&self) -> u64 { self.number }
/// Generate an inclusion proof for the entry at a specific block.
/// Nodes before level `from_level` will be omitted.
/// Returns an error on an incomplete trie, and `Ok(None)` on an unprovable request.
pub fn prove(&self, num: u64, from_level: u32) -> trie::Result<Option<Vec<Bytes>>> {
if block_to_cht_number(num) != Some(self.number) { return Ok(None) }
let mut recorder = Recorder::with_depth(from_level);
let t = TrieDB::new(&self.db, &self.root)?;
t.get_with(&key!(num), &mut recorder)?;
Ok(Some(recorder.drain().into_iter().map(|x| x.data).collect()))
}
}
/// Block information necessary to build a CHT.
pub struct BlockInfo {
/// The block's hash.
pub hash: H256,
/// The block's parent's hash.
pub parent_hash: H256,
/// The block's total difficulty.
pub total_difficulty: U256,
}
/// Build an in-memory CHT from a closure which provides necessary information
/// about blocks. If the fetcher ever fails to provide the info, the CHT
/// will not be generated.
pub fn build<F>(cht_num: u64, mut fetcher: F) -> Option<CHT<MemoryDB>>
where F: FnMut(BlockId) -> Option<BlockInfo>
{
let mut db = MemoryDB::new();
// start from the last block by number and work backwards.
let last_num = start_number(cht_num + 1) - 1;
let mut id = BlockId::Number(last_num);
let mut root = H256::default();
{
let mut t = TrieDBMut::new(&mut db, &mut root);
for blk_num in (0..SIZE).map(|n| last_num - n) {
let info = match fetcher(id) {
Some(info) => info,
None => return None,
};
id = BlockId::Hash(info.parent_hash);
t.insert(&key!(blk_num), &val!(info.hash, info.total_difficulty))
.expect("fresh in-memory database is infallible; qed");
}
}
Some(CHT {
db: db,
root: root,
number: cht_num,
})
}
/// Compute a CHT root from an iterator of (hash, td) pairs. Fails if shorter than
/// SIZE items. The items are assumed to proceed sequentially from `start_number(cht_num)`.
/// Discards the trie's nodes.
pub fn compute_root<I>(cht_num: u64, iterable: I) -> Option<H256>
where I: IntoIterator<Item=(H256, U256)>
{
let mut v = Vec::with_capacity(SIZE as usize);
let start_num = start_number(cht_num) as usize;
for (i, (h, td)) in iterable.into_iter().take(SIZE as usize).enumerate() {
v.push((key!(i + start_num).to_vec(), val!(h, td).to_vec()))
}
if v.len() == SIZE as usize {
Some(::util::triehash::trie_root(v))
} else {
None
}
}
/// Convert a block number to a CHT number.
/// Returns `None` for `block_num` == 0, `Some` otherwise.
pub fn block_to_cht_number(block_num: u64) -> Option<u64> {
@ -37,6 +157,12 @@ pub fn start_number(cht_num: u64) -> u64 {
#[cfg(test)]
mod tests {
#[test]
fn size_is_lt_usize() {
// to ensure safe casting on the target platform.
assert!(::cht::SIZE < usize::max_value() as u64)
}
#[test]
fn block_to_cht_number() {
assert!(::cht::block_to_cht_number(0).is_none());

View File

@ -173,26 +173,34 @@ impl HeaderChain {
// produce next CHT root if it's time.
let earliest_era = *candidates.keys().next().expect("at least one era just created; qed");
if earliest_era + HISTORY + cht::SIZE <= number {
let mut values = Vec::with_capacity(cht::SIZE as usize);
{
let mut headers = self.headers.write();
for i in (0..cht::SIZE).map(|x| x + earliest_era) {
let cht_num = cht::block_to_cht_number(earliest_era)
.expect("fails only for number == 0; genesis never imported; qed");
debug_assert_eq!(cht_num as usize, self.cht_roots.lock().len());
let mut headers = self.headers.write();
let cht_root = {
let mut i = earliest_era;
// iterable function which removes the candidates as it goes
// along. this will only be called until the CHT is complete.
let iter = || {
let era_entry = candidates.remove(&i)
.expect("all eras are sequential with no gaps; qed");
i += 1;
for ancient in &era_entry.candidates {
headers.remove(&ancient.hash);
}
values.push((
::rlp::encode(&i).to_vec(),
::rlp::encode(&era_entry.canonical_hash).to_vec(),
));
}
}
let canon = &era_entry.candidates[0];
(canon.hash, canon.total_difficulty)
};
cht::compute_root(cht_num, ::itertools::repeat_call(iter))
.expect("fails only when too few items; this is checked; qed")
};
let cht_root = ::util::triehash::trie_root(values);
debug!(target: "chain", "Produced CHT {} root: {:?}", (earliest_era - 1) % cht::SIZE, cht_root);
debug!(target: "chain", "Produced CHT {} root: {:?}", cht_num, cht_root);
self.cht_roots.lock().push(cht_root);
}

View File

@ -21,8 +21,9 @@ use ethcore::block_status::BlockStatus;
use ethcore::client::ClientReport;
use ethcore::ids::BlockId;
use ethcore::header::Header;
use ethcore::views::HeaderView;
use ethcore::verification::queue::{self, HeaderQueue};
use ethcore::transaction::PendingTransaction;
use ethcore::transaction::{PendingTransaction, Condition as TransactionCondition};
use ethcore::blockchain_info::BlockChainInfo;
use ethcore::spec::Spec;
use ethcore::service::ClientIoMessage;
@ -34,6 +35,7 @@ use util::{Bytes, Mutex, RwLock};
use provider::Provider;
use request;
use time;
use self::header_chain::HeaderChain;
@ -110,7 +112,11 @@ impl Client {
let best_num = self.chain.best_block().number;
self.tx_pool.lock()
.values()
.filter(|t| t.min_block.as_ref().map_or(true, |x| x <= &best_num))
.filter(|t| match t.condition {
Some(TransactionCondition::Number(ref x)) => x <= &best_num,
Some(TransactionCondition::Timestamp(ref x)) => *x <= time::get_time().sec as u64,
None => true,
})
.cloned()
.collect()
}
@ -135,6 +141,7 @@ impl Client {
genesis_hash: genesis_hash,
best_block_hash: best_block.hash,
best_block_number: best_block.number,
best_block_timestamp: HeaderView::new(&self.chain.get_header(BlockId::Latest).expect("Latest hash is always in the chain")).timestamp(),
ancient_block_hash: if first_block.is_some() { Some(genesis_hash) } else { None },
ancient_block_number: if first_block.is_some() { Some(0) } else { None },
first_block_hash: first_block.as_ref().map(|first| first.hash),

View File

@ -68,6 +68,7 @@ extern crate smallvec;
extern crate time;
extern crate futures;
extern crate rand;
extern crate itertools;
#[cfg(feature = "ipc")]
extern crate ethcore_ipc as ipc;

View File

@ -23,6 +23,8 @@ use ethcore::transaction::PendingTransaction;
use ethcore::ids::BlockId;
use ethcore::encoded;
use cht::{self, BlockInfo};
use util::{Bytes, H256};
use request;
@ -227,48 +229,54 @@ impl<T: ProvingBlockChainClient + ?Sized> Provider for T {
}
fn header_proof(&self, req: request::HeaderProof) -> Option<(encoded::Header, Vec<Bytes>)> {
use util::MemoryDB;
use util::trie::{Trie, TrieMut, TrieDB, TrieDBMut, Recorder};
if Some(req.cht_number) != ::cht::block_to_cht_number(req.block_number) {
if Some(req.cht_number) != cht::block_to_cht_number(req.block_number) {
debug!(target: "les_provider", "Requested CHT number mismatch with block number.");
return None;
}
let mut memdb = MemoryDB::new();
let mut root = H256::default();
let mut needed_hdr = None;
{
let mut t = TrieDBMut::new(&mut memdb, &mut root);
let start_num = ::cht::start_number(req.cht_number);
for i in (0..::cht::SIZE).map(|x| x + start_num) {
match self.block_header(BlockId::Number(i)) {
None => return None,
Some(hdr) => {
t.insert(
&*::rlp::encode(&i),
&*::rlp::encode(&hdr.hash()),
).expect("fresh in-memory database is infallible; qed");
if i == req.block_number { needed_hdr = Some(hdr) }
// build the CHT, caching the requested header as we pass through it.
let cht = {
let block_info = |id| {
let hdr = self.block_header(id);
let td = self.block_total_difficulty(id);
match (hdr, td) {
(Some(hdr), Some(td)) => {
let info = BlockInfo {
hash: hdr.hash(),
parent_hash: hdr.parent_hash(),
total_difficulty: td,
};
if hdr.number() == req.block_number {
needed_hdr = Some(hdr);
}
Some(info)
}
_ => None,
}
};
match cht::build(req.cht_number, block_info) {
Some(cht) => cht,
None => return None, // incomplete CHT.
}
}
};
let needed_hdr = needed_hdr.expect("`needed_hdr` always set in loop, number checked before; qed");
let mut recorder = Recorder::with_depth(req.from_level);
let t = TrieDB::new(&memdb, &root)
.expect("Same DB and root as just produced by TrieDBMut; qed");
if let Err(e) = t.get_with(&*::rlp::encode(&req.block_number), &mut recorder) {
debug!(target: "les_provider", "Error looking up number in freshly-created CHT: {}", e);
return None;
// prove our result.
match cht.prove(req.block_number, req.from_level) {
Ok(Some(proof)) => Some((needed_hdr, proof)),
Ok(None) => None,
Err(e) => {
debug!(target: "les_provider", "Error looking up number in freshly-created CHT: {}", e);
None
}
}
// TODO: cache calculated CHT if possible.
let proof = recorder.drain().into_iter().map(|x| x.data).collect();
Some((needed_hdr, proof))
}
fn ready_transactions(&self) -> Vec<PendingTransaction> {

View File

@ -9,7 +9,11 @@
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1",
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e"
]
}
},
"timeoutPropose": 10000,
"timeoutPrevote": 10000,
"timeoutPrecommit": 10000,
"timeoutCommit": 10000
}
}
},

View File

@ -21,7 +21,7 @@ mod stores;
use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy};
use std::fmt;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::time::{Instant, Duration};
use util::RwLock;
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
@ -114,8 +114,8 @@ impl AccountProvider {
pub fn new(sstore: Box<SecretStore>) -> Self {
AccountProvider {
unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
address_book: RwLock::new(AddressBook::new(&sstore.local_path())),
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
sstore: sstore,
transient_sstore: transient_sstore(),
}
@ -216,7 +216,7 @@ impl AccountProvider {
Some(accounts) => Ok(accounts),
None => match dapps.policy() {
NewDappsPolicy::AllAccounts => self.accounts(),
NewDappsPolicy::Whitelist(accounts) => Ok(accounts),
NewDappsPolicy::Whitelist(accounts) => self.filter_addresses(accounts),
}
}
}
@ -231,28 +231,42 @@ impl AccountProvider {
/// Sets addresses visile for dapp.
pub fn set_dapps_addresses(&self, dapp: DappId, addresses: Vec<Address>) -> Result<(), Error> {
let addresses = self.filter_addresses(addresses)?;
self.dapps_settings.write().set_accounts(dapp, addresses);
Ok(())
}
/// Returns each address along with metadata.
pub fn addresses_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
Ok(self.address_book.read().get())
/// Removes addresses that are neither accounts nor in address book.
fn filter_addresses(&self, addresses: Vec<Address>) -> Result<Vec<Address>, Error> {
let valid = self.addresses_info().into_iter()
.map(|(address, _)| address)
.chain(self.accounts()?)
.collect::<HashSet<_>>();
Ok(addresses.into_iter()
.filter(|a| valid.contains(&a))
.collect()
)
}
/// Returns each address along with metadata.
pub fn set_address_name(&self, account: Address, name: String) -> Result<(), Error> {
Ok(self.address_book.write().set_name(account, name))
pub fn addresses_info(&self) -> HashMap<Address, AccountMeta> {
self.address_book.read().get()
}
/// Returns each address along with metadata.
pub fn set_address_meta(&self, account: Address, meta: String) -> Result<(), Error> {
Ok(self.address_book.write().set_meta(account, meta))
pub fn set_address_name(&self, account: Address, name: String) {
self.address_book.write().set_name(account, name)
}
/// Returns each address along with metadata.
pub fn set_address_meta(&self, account: Address, meta: String) {
self.address_book.write().set_meta(account, meta)
}
/// Removes and address from the addressbook
pub fn remove_address(&self, addr: Address) -> Result<(), Error> {
Ok(self.address_book.write().remove(addr))
pub fn remove_address(&self, addr: Address) {
self.address_book.write().remove(addr)
}
/// Returns each account along with name and meta.
@ -502,9 +516,12 @@ mod tests {
let app = DappId("app1".into());
// set `AllAccounts` policy
ap.set_new_dapps_whitelist(None).unwrap();
// add accounts to address book
ap.set_address_name(1.into(), "1".into());
ap.set_address_name(2.into(), "2".into());
// when
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into(), 3.into()]).unwrap();
// then
assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]);
@ -515,6 +532,7 @@ mod tests {
// given
let ap = AccountProvider::transient_provider();
let address = ap.new_account("test").unwrap();
ap.set_address_name(1.into(), "1".into());
// When returning nothing
ap.set_new_dapps_whitelist(Some(vec![])).unwrap();
@ -524,6 +542,10 @@ mod tests {
ap.set_new_dapps_whitelist(None).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![address]);
// change to non-existent account
ap.set_new_dapps_whitelist(Some(vec![2.into()])).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![]);
// change to a whitelist
ap.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap();
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![1.into()]);

View File

@ -19,7 +19,7 @@
use std::{fs, fmt, hash, ops};
use std::sync::atomic::{self, AtomicUsize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use ethstore::ethkey::Address;
use ethjson::misc::{
@ -37,9 +37,9 @@ pub struct AddressBook {
impl AddressBook {
/// Creates new address book at given directory.
pub fn new(path: String) -> Self {
pub fn new(path: &Path) -> Self {
let mut r = AddressBook {
cache: DiskMap::new(path, "address_book.json".into())
cache: DiskMap::new(path, "address_book.json")
};
r.cache.revert(AccountMeta::read);
r
@ -200,11 +200,11 @@ pub struct DappsSettingsStore {
impl DappsSettingsStore {
/// Creates new store at given directory path.
pub fn new(path: String) -> Self {
pub fn new(path: &Path) -> Self {
let mut r = DappsSettingsStore {
settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()),
policy: DiskMap::new(path.clone(), "dapps_policy.json".into()),
history: DiskMap::new(path.clone(), "dapps_history.json".into()),
settings: DiskMap::new(path, "dapps_accounts.json".into()),
policy: DiskMap::new(path, "dapps_policy.json".into()),
history: DiskMap::new(path, "dapps_history.json".into()),
time: TimeProvider::Clock,
};
r.settings.revert(JsonSettings::read);
@ -297,9 +297,8 @@ impl<K: hash::Hash + Eq, V> ops::DerefMut for DiskMap<K, V> {
}
impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
pub fn new(path: String, file_name: String) -> Self {
trace!(target: "diskmap", "new({})", path);
let mut path: PathBuf = path.into();
pub fn new(path: &Path, file_name: &str) -> Self {
let mut path = path.to_owned();
path.push(file_name);
trace!(target: "diskmap", "path={:?}", path);
DiskMap {
@ -310,7 +309,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
}
pub fn transient() -> Self {
let mut map = DiskMap::new(Default::default(), "diskmap.json".into());
let mut map = DiskMap::new(&PathBuf::new(), "diskmap.json".into());
map.transient = true;
map
}
@ -354,27 +353,25 @@ mod tests {
#[test]
fn should_save_and_reload_address_book() {
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = AddressBook::new(path.clone());
let path = RandomTempPath::create_dir();
let mut b = AddressBook::new(&path);
b.set_name(1.into(), "One".to_owned());
b.set_meta(1.into(), "{1:1}".to_owned());
let b = AddressBook::new(path);
let b = AddressBook::new(&path);
assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]);
}
#[test]
fn should_remove_address() {
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = AddressBook::new(path.clone());
let path = RandomTempPath::create_dir();
let mut b = AddressBook::new(&path);
b.set_name(1.into(), "One".to_owned());
b.set_name(2.into(), "Two".to_owned());
b.set_name(3.into(), "Three".to_owned());
b.remove(2.into());
let b = AddressBook::new(path);
let b = AddressBook::new(&path);
assert_eq!(b.get(), hash_map![
1.into() => AccountMeta{name: "One".to_owned(), meta: "{}".to_owned(), uuid: None},
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
@ -384,15 +381,14 @@ mod tests {
#[test]
fn should_save_and_reload_dapps_settings() {
// given
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = DappsSettingsStore::new(path.clone());
let path = RandomTempPath::create_dir();
let mut b = DappsSettingsStore::new(&path);
// when
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
// then
let b = DappsSettingsStore::new(path);
let b = DappsSettingsStore::new(&path);
assert_eq!(b.settings(), hash_map![
"dappOne".into() => DappsSettings {
accounts: vec![1.into(), 2.into()],
@ -422,9 +418,8 @@ mod tests {
#[test]
fn should_store_dapps_policy() {
// given
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut store = DappsSettingsStore::new(path.clone());
let path = RandomTempPath::create_dir();
let mut store = DappsSettingsStore::new(&path);
// Test default policy
assert_eq!(store.policy(), NewDappsPolicy::AllAccounts);
@ -433,7 +428,7 @@ mod tests {
store.set_policy(NewDappsPolicy::Whitelist(vec![1.into(), 2.into()]));
// then
let store = DappsSettingsStore::new(path);
let store = DappsSettingsStore::new(&path);
assert_eq!(store.policy.clone(), hash_map![
"default".into() => NewDappsPolicy::Whitelist(vec![1.into(), 2.into()])
]);

View File

@ -24,6 +24,8 @@ pub struct BestBlock {
pub hash: H256,
/// Best block number.
pub number: BlockNumber,
/// Best block timestamp.
pub timestamp: u64,
/// Best block total difficulty.
pub total_difficulty: U256,
/// Best block uncompressed bytes

View File

@ -485,6 +485,7 @@ impl BlockChain {
let best_block_number = bc.block_number(&best_block_hash).unwrap();
let best_block_total_difficulty = bc.block_details(&best_block_hash).unwrap().total_difficulty;
let best_block_rlp = bc.block(&best_block_hash).unwrap().into_inner();
let best_block_timestamp = BlockView::new(&best_block_rlp).header().timestamp();
let raw_first = bc.db.get(db::COL_EXTRA, b"first").unwrap().map(|v| v.to_vec());
let mut best_ancient = bc.db.get(db::COL_EXTRA, b"ancient").unwrap().map(|h| H256::from_slice(&h));
@ -533,6 +534,7 @@ impl BlockChain {
number: best_block_number,
total_difficulty: best_block_total_difficulty,
hash: best_block_hash,
timestamp: best_block_timestamp,
block: best_block_rlp,
};
@ -585,6 +587,7 @@ impl BlockChain {
number: extras.number - 1,
total_difficulty: best_block_total_difficulty,
hash: hash,
timestamp: BlockView::new(&best_block_rlp).header().timestamp(),
block: best_block_rlp,
};
// update parent extras
@ -738,6 +741,7 @@ impl BlockChain {
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
info: info,
timestamp: header.timestamp(),
block: bytes
}, is_best);
@ -786,6 +790,7 @@ impl BlockChain {
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
info: info,
timestamp: header.timestamp(),
block: bytes,
}, is_best);
true
@ -850,6 +855,7 @@ impl BlockChain {
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
info: info.clone(),
timestamp: header.timestamp(),
block: bytes,
}, true);
@ -921,6 +927,7 @@ impl BlockChain {
hash: update.info.hash,
number: update.info.number,
total_difficulty: update.info.total_difficulty,
timestamp: update.timestamp,
block: update.block.to_vec(),
});
},
@ -1206,6 +1213,11 @@ impl BlockChain {
self.best_block.read().number
}
/// Get best block timestamp.
pub fn best_block_timestamp(&self) -> u64 {
self.best_block.read().timestamp
}
/// Get best block total difficulty.
pub fn best_block_total_difficulty(&self) -> U256 {
self.best_block.read().total_difficulty
@ -1293,6 +1305,7 @@ impl BlockChain {
genesis_hash: self.genesis_hash(),
best_block_hash: best_block.hash.clone(),
best_block_number: best_block.number,
best_block_timestamp: best_block.timestamp,
first_block_hash: self.first_block(),
first_block_number: From::from(self.first_block_number()),
ancient_block_hash: best_ancient_block.as_ref().map(|b| b.hash.clone()),

View File

@ -9,6 +9,8 @@ use super::extras::{BlockDetails, BlockReceipts, TransactionAddress, LogGroupPos
pub struct ExtrasUpdate<'a> {
/// Block info.
pub info: BlockInfo,
/// Block timestamp.
pub timestamp: u64,
/// Current block uncompressed rlp bytes
pub block: &'a [u8],
/// Modified block hashes.

View File

@ -1406,7 +1406,11 @@ impl BlockChainClient for Client {
}
fn ready_transactions(&self) -> Vec<PendingTransaction> {
self.miner.ready_transactions(self.chain.read().best_block_number())
let (number, timestamp) = {
let chain = self.chain.read();
(chain.best_block_number(), chain.best_block_timestamp())
};
self.miner.ready_transactions(number, timestamp)
}
fn queue_consensus_message(&self, message: Bytes) {

View File

@ -552,7 +552,7 @@ impl BlockChainClient for TestBlockChainClient {
let mut adding = false;
let mut blocks = Vec::new();
for (_, hash) in numbers_read.iter().sort_by(|tuple1, tuple2| tuple1.0.cmp(tuple2.0)) {
for (_, hash) in numbers_read.iter().sorted_by(|tuple1, tuple2| tuple1.0.cmp(tuple2.0)) {
if hash == to {
if adding {
blocks.push(hash.clone());
@ -669,12 +669,14 @@ impl BlockChainClient for TestBlockChainClient {
}
fn chain_info(&self) -> BlockChainInfo {
let number = self.blocks.read().len() as BlockNumber - 1;
BlockChainInfo {
total_difficulty: *self.difficulty.read(),
pending_total_difficulty: *self.difficulty.read(),
genesis_hash: self.genesis_hash.clone(),
best_block_hash: self.last_hash.read().clone(),
best_block_number: self.blocks.read().len() as BlockNumber - 1,
best_block_number: number,
best_block_timestamp: number,
first_block_hash: self.first_block.read().as_ref().map(|x| x.0),
first_block_number: self.first_block.read().as_ref().map(|x| x.1),
ancient_block_hash: self.ancient_block.read().as_ref().map(|x| x.0),
@ -709,7 +711,8 @@ impl BlockChainClient for TestBlockChainClient {
}
fn ready_transactions(&self) -> Vec<PendingTransaction> {
self.miner.ready_transactions(self.chain_info().best_block_number)
let info = self.chain_info();
self.miner.ready_transactions(info.best_block_number, info.best_block_timestamp)
}
fn signing_network_id(&self) -> Option<u64> { None }

View File

@ -159,7 +159,7 @@ impl IoHandler<()> for TransitionHandler {
fn initialize(&self, io: &IoContext<()>) {
if let Some(engine) = self.engine.upgrade() {
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
.unwrap_or_else(|e| warn!(target: "poa", "Failed to start consensus step timer: {}.", e))
.unwrap_or_else(|e| warn!(target: "engine", "Failed to start consensus step timer: {}.", e))
}
}
@ -168,7 +168,7 @@ impl IoHandler<()> for TransitionHandler {
if let Some(engine) = self.engine.upgrade() {
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))
.unwrap_or_else(|e| warn!(target: "engine", "Failed to restart consensus step timer: {}.", e))
}
}
}
@ -234,14 +234,14 @@ impl Engine for AuthorityRound {
let step = self.step.load(AtomicOrdering::SeqCst);
if self.is_step_proposer(step, header.author()) {
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
trace!(target: "engine", "generate_seal: Issuing a block for step {}.", step);
self.proposed.store(true, AtomicOrdering::SeqCst);
return Seal::Regular(vec![encode(&step).to_vec(), encode(&(&H520::from(signature) as &[u8])).to_vec()]);
} else {
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
warn!(target: "engine", "generate_seal: FAIL: Accounts secret key unavailable.");
}
} else {
trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step);
trace!(target: "engine", "generate_seal: Not a proposer for step {}.", step);
}
Seal::None
}
@ -260,7 +260,7 @@ impl Engine for AuthorityRound {
/// Check the number of seal fields.
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
if header.seal().len() != self.seal_fields() {
trace!(target: "poa", "verify_block_basic: wrong number of seal fields");
trace!(target: "engine", "verify_block_basic: wrong number of seal fields");
Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: header.seal().len() }
)))
@ -279,11 +279,11 @@ impl Engine for AuthorityRound {
if verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? {
Ok(())
} else {
trace!(target: "poa", "verify_block_unordered: bad proposer for step: {}", header_step);
trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", header_step);
Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))?
}
} else {
trace!(target: "poa", "verify_block_unordered: block from the future");
trace!(target: "engine", "verify_block_unordered: block from the future");
self.validators.report_benign(header.author());
Err(BlockError::InvalidSeal)?
}
@ -297,7 +297,7 @@ impl Engine for AuthorityRound {
let step = header_step(header)?;
// Check if parent is from a previous step.
if step == header_step(parent)? {
trace!(target: "poa", "Multiple blocks proposed for step {}.", step);
trace!(target: "engine", "Multiple blocks proposed for step {}.", step);
self.validators.report_malicious(header.author());
Err(EngineError::DoubleVote(header.author().clone()))?;
}

View File

@ -159,13 +159,13 @@ impl Tendermint {
let message = ConsensusMessage::new(signature, h, r, *s, block_hash);
let validator = self.signer.address();
self.votes.vote(message.clone(), &validator);
debug!(target: "poa", "Generated {:?} as {}.", message, validator);
debug!(target: "engine", "Generated {:?} as {}.", message, validator);
self.handle_valid_message(&message);
Some(message_rlp)
},
Err(e) => {
trace!(target: "poa", "Could not sign the message {}", e);
trace!(target: "engine", "Could not sign the message {}", e);
None
},
}
@ -186,7 +186,7 @@ impl Tendermint {
fn to_next_height(&self, height: Height) {
let new_height = height + 1;
debug!(target: "poa", "Received a Commit, transitioning to height {}.", new_height);
debug!(target: "engine", "Received a Commit, transitioning to height {}.", new_height);
self.last_lock.store(0, AtomicOrdering::SeqCst);
self.height.store(new_height, AtomicOrdering::SeqCst);
self.view.store(0, AtomicOrdering::SeqCst);
@ -196,7 +196,7 @@ impl Tendermint {
/// Use via step_service to transition steps.
fn to_step(&self, step: Step) {
if let Err(io_err) = self.step_service.send_message(step) {
warn!(target: "poa", "Could not proceed to step {}.", io_err)
warn!(target: "engine", "Could not proceed to step {}.", io_err)
}
*self.step.write() = step;
match step {
@ -212,10 +212,10 @@ impl Tendermint {
self.generate_and_broadcast_message(block_hash);
},
Step::Precommit => {
trace!(target: "poa", "to_step: Precommit.");
trace!(target: "engine", "to_step: Precommit.");
let block_hash = match *self.lock_change.read() {
Some(ref m) if self.is_view(m) && m.block_hash.is_some() => {
trace!(target: "poa", "Setting last lock: {}", m.vote_step.view);
trace!(target: "engine", "Setting last lock: {}", m.vote_step.view);
self.last_lock.store(m.vote_step.view, AtomicOrdering::SeqCst);
m.block_hash
},
@ -224,7 +224,7 @@ impl Tendermint {
self.generate_and_broadcast_message(block_hash);
},
Step::Commit => {
trace!(target: "poa", "to_step: Commit.");
trace!(target: "engine", "to_step: Commit.");
// Commit the block using a complete signature set.
let view = self.view.load(AtomicOrdering::SeqCst);
let height = self.height.load(AtomicOrdering::SeqCst);
@ -234,7 +234,7 @@ impl Tendermint {
let proposal_step = VoteStep::new(height, view, Step::Propose);
let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit);
if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) {
trace!(target: "poa", "Collected seal: {:?}", seal);
trace!(target: "engine", "Collected seal: {:?}", seal);
let seal = vec![
::rlp::encode(&view).to_vec(),
::rlp::encode(&seal.proposal).to_vec(),
@ -243,7 +243,7 @@ impl Tendermint {
self.submit_seal(block_hash, seal);
self.to_next_height(height);
} else {
warn!(target: "poa", "Not enough votes found!");
warn!(target: "engine", "Not enough votes found!");
}
}
}
@ -262,7 +262,7 @@ impl Tendermint {
/// Find the designated for the given view.
fn view_proposer(&self, height: Height, view: View) -> Address {
let proposer_nonce = height + view;
trace!(target: "poa", "Proposer nonce: {}", proposer_nonce);
trace!(target: "engine", "Proposer nonce: {}", proposer_nonce);
self.validators.get(proposer_nonce)
}
@ -291,7 +291,7 @@ impl Tendermint {
}
fn increment_view(&self, n: View) {
trace!(target: "poa", "increment_view: New view.");
trace!(target: "engine", "increment_view: New view.");
self.view.fetch_add(n, AtomicOrdering::SeqCst);
}
@ -331,7 +331,7 @@ impl Tendermint {
&& message.block_hash.is_some()
&& self.has_enough_aligned_votes(message);
if lock_change {
trace!(target: "poa", "handle_valid_message: Lock change.");
trace!(target: "engine", "handle_valid_message: Lock change.");
*self.lock_change.write() = Some(message.clone());
}
// Check if it can affect the step transition.
@ -349,7 +349,7 @@ impl Tendermint {
self.increment_view(vote_step.view - self.view.load(AtomicOrdering::SeqCst));
Some(Step::Precommit)
},
// Avoid counting twice.
// Avoid counting votes twice.
Step::Prevote if lock_change => Some(Step::Precommit),
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
Step::Prevote if self.has_enough_future_step_votes(&vote_step) => {
@ -360,7 +360,7 @@ impl Tendermint {
};
if let Some(step) = next_step {
trace!(target: "poa", "Transition to {:?} triggered.", step);
trace!(target: "engine", "Transition to {:?} triggered.", step);
self.to_step(step);
}
}
@ -429,7 +429,7 @@ impl Engine for Tendermint {
let vote_info = message_info_rlp(&VoteStep::new(height, view, Step::Propose), bh.clone());
if let Ok(signature) = self.signer.sign(vote_info.sha3()).map(Into::into) {
// Insert Propose vote.
debug!(target: "poa", "Submitting proposal {} at height {} view {}.", header.bare_hash(), height, view);
debug!(target: "engine", "Submitting proposal {} at height {} view {}.", header.bare_hash(), height, view);
self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author);
// Remember proposal for later seal submission.
*self.proposal.write() = bh;
@ -439,7 +439,7 @@ impl Engine for Tendermint {
::rlp::EMPTY_LIST_RLP.to_vec()
])
} else {
warn!(target: "poa", "generate_seal: FAIL: accounts secret key unavailable");
warn!(target: "engine", "generate_seal: FAIL: accounts secret key unavailable");
Seal::None
}
}
@ -457,7 +457,7 @@ impl Engine for Tendermint {
self.validators.report_malicious(&sender);
Err(EngineError::DoubleVote(sender))?
}
trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender);
trace!(target: "engine", "Handling a valid {:?} from {}.", message, sender);
self.handle_valid_message(&message);
}
Ok(())
@ -519,7 +519,7 @@ impl Engine for Tendermint {
if origins.insert(address) {
signature_count += 1;
} else {
warn!(target: "poa", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
warn!(target: "engine", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
Err(BlockError::InvalidSeal)?;
}
}
@ -577,12 +577,12 @@ impl Engine for Tendermint {
let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed");
if signatures_len != 1 {
// New Commit received, skip to next height.
trace!(target: "poa", "Received a commit: {:?}.", proposal.vote_step);
trace!(target: "engine", "Received a commit: {:?}.", proposal.vote_step);
self.to_next_height(proposal.vote_step.height);
return false;
}
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
debug!(target: "poa", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
debug!(target: "engine", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
if self.is_view(&proposal) {
*self.proposal.write() = proposal.block_hash.clone();
}
@ -594,7 +594,7 @@ impl Engine for Tendermint {
fn step(&self) {
let next_step = match *self.step.read() {
Step::Propose => {
trace!(target: "poa", "Propose timeout.");
trace!(target: "engine", "Propose timeout.");
if self.proposal.read().is_none() {
// Report the proposer if no proposal was received.
let current_proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
@ -603,26 +603,26 @@ impl Engine for Tendermint {
Step::Prevote
},
Step::Prevote if self.has_enough_any_votes() => {
trace!(target: "poa", "Prevote timeout.");
trace!(target: "engine", "Prevote timeout.");
Step::Precommit
},
Step::Prevote => {
trace!(target: "poa", "Prevote timeout without enough votes.");
trace!(target: "engine", "Prevote timeout without enough votes.");
self.broadcast_old_messages();
Step::Prevote
},
Step::Precommit if self.has_enough_any_votes() => {
trace!(target: "poa", "Precommit timeout.");
trace!(target: "engine", "Precommit timeout.");
self.increment_view(1);
Step::Propose
},
Step::Precommit => {
trace!(target: "poa", "Precommit timeout without enough votes.");
trace!(target: "engine", "Precommit timeout without enough votes.");
self.broadcast_old_messages();
Step::Precommit
},
Step::Commit => {
trace!(target: "poa", "Commit timeout.");
trace!(target: "engine", "Commit timeout.");
Step::Propose
},
};
@ -838,7 +838,6 @@ mod tests {
let (b, seal) = propose_default(&spec, proposer);
assert!(b.lock().try_seal(spec.engine.as_ref(), seal).is_ok());
spec.engine.stop();
}
#[test]
@ -850,7 +849,6 @@ mod tests {
let (b, seal) = propose_default(&spec, proposer);
let sealed = b.lock().seal(spec.engine.as_ref(), seal).unwrap();
assert!(spec.engine.is_proposal(sealed.header()));
spec.engine.stop();
}
#[test]
@ -858,7 +856,7 @@ mod tests {
let (spec, tap) = setup();
let engine = spec.engine.clone();
let v0 = insert_and_register(&tap, engine.as_ref(), "0");
let v0 = insert_and_unlock(&tap, "0");
let v1 = insert_and_register(&tap, engine.as_ref(), "1");
let h = 1;
@ -883,7 +881,6 @@ mod tests {
assert!(notify.messages.read().contains(&prevote_current));
assert!(notify.messages.read().contains(&precommit_current));
assert!(notify.messages.read().contains(&prevote_future));
engine.stop();
}
#[test]
@ -933,7 +930,5 @@ mod tests {
// Last precommit.
vote(engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
assert_eq!(client.chain_info().best_block_number, 1);
engine.stop();
}
}

View File

@ -56,7 +56,9 @@ fn set_timeout<S: Sync + Send + Clone>(io: &IoContext<S>, timeout: Duration) {
impl <S> IoHandler<S> for TransitionHandler<S> where S: Sync + Send + Clone + 'static {
fn initialize(&self, io: &IoContext<S>) {
set_timeout(io, self.timeouts.initial());
let initial = self.timeouts.initial();
trace!(target: "engine", "Setting the initial timeout to {}.", initial);
set_timeout(io, initial);
}
/// Call step after timeout.

View File

@ -118,13 +118,13 @@ impl <M: Message + Default + Encodable + Debug> VoteCollector<M> {
.get(&message.round())
.map_or(false, |c| {
let is_known = c.messages.contains(message);
if is_known { trace!(target: "poa", "Known message: {:?}.", message); }
if is_known { trace!(target: "engine", "Known message: {:?}.", message); }
is_known
})
|| {
let guard = self.votes.read();
let is_old = guard.keys().next().map_or(true, |oldest| message.round() <= oldest);
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
if is_old { trace!(target: "engine", "Old message {:?}.", message); }
is_old
}
}

View File

@ -25,7 +25,7 @@ use client::TransactionImportResult;
use executive::contract_address;
use block::{ClosedBlock, IsBlock, Block};
use error::*;
use transaction::{Action, UnverifiedTransaction, PendingTransaction, SignedTransaction};
use transaction::{Action, UnverifiedTransaction, PendingTransaction, SignedTransaction, Condition as TransactionCondition};
use receipt::{Receipt, RichReceipt};
use spec::Spec;
use engines::{Engine, Seal};
@ -325,7 +325,7 @@ impl Miner {
let _timer = PerfTimer::new("prepare_block");
let chain_info = chain.chain_info();
let (transactions, mut open_block, original_work_hash) = {
let transactions = {self.transaction_queue.lock().top_transactions_at(chain_info.best_block_number)};
let transactions = {self.transaction_queue.lock().top_transactions_at(chain_info.best_block_number, chain_info.best_block_timestamp)};
let mut sealing_work = self.sealing_work.lock();
let last_work_hash = sealing_work.queue.peek_last_ref().map(|pb| pb.block().fields().header.hash());
let best_hash = chain_info.best_block_hash;
@ -597,7 +597,7 @@ impl Miner {
client: &MiningBlockChainClient,
transactions: Vec<UnverifiedTransaction>,
default_origin: TransactionOrigin,
min_block: Option<BlockNumber>,
condition: Option<TransactionCondition>,
transaction_queue: &mut BanningTransactionQueue,
) -> Vec<Result<TransactionImportResult, Error>> {
let accounts = self.accounts.as_ref()
@ -635,7 +635,7 @@ impl Miner {
let details_provider = TransactionDetailsProvider::new(client, &self.service_transaction_action);
match origin {
TransactionOrigin::Local | TransactionOrigin::RetractedBlock => {
transaction_queue.add(transaction, origin, insertion_time, min_block, &details_provider)
transaction_queue.add(transaction, origin, insertion_time, condition.clone(), &details_provider)
},
TransactionOrigin::External => {
transaction_queue.add_with_banlist(transaction, insertion_time, &details_provider)
@ -892,7 +892,7 @@ impl MinerService for Miner {
let mut transaction_queue = self.transaction_queue.lock();
// We need to re-validate transactions
let import = self.add_transactions_to_queue(
chain, vec![pending.transaction.into()], TransactionOrigin::Local, pending.min_block, &mut transaction_queue
chain, vec![pending.transaction.into()], TransactionOrigin::Local, pending.condition, &mut transaction_queue
).pop().expect("one result returned per added transaction; one added => one result; qed");
match import {
@ -927,7 +927,7 @@ impl MinerService for Miner {
fn pending_transactions(&self) -> Vec<PendingTransaction> {
let queue = self.transaction_queue.lock();
queue.pending_transactions(BlockNumber::max_value())
queue.pending_transactions(BlockNumber::max_value(), u64::max_value())
}
fn local_transactions(&self) -> BTreeMap<H256, LocalTransactionStatus> {
@ -942,14 +942,14 @@ impl MinerService for Miner {
self.transaction_queue.lock().future_transactions()
}
fn ready_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction> {
fn ready_transactions(&self, best_block: BlockNumber, best_block_timestamp: u64) -> Vec<PendingTransaction> {
let queue = self.transaction_queue.lock();
match self.options.pending_set {
PendingSet::AlwaysQueue => queue.pending_transactions(best_block),
PendingSet::AlwaysQueue => queue.pending_transactions(best_block, best_block_timestamp),
PendingSet::SealingOrElseQueue => {
self.from_pending_block(
best_block,
|| queue.pending_transactions(best_block),
|| queue.pending_transactions(best_block, best_block_timestamp),
|sealing| sealing.transactions().iter().map(|t| t.clone().into()).collect()
)
},
@ -1325,7 +1325,7 @@ mod tests {
// then
assert_eq!(res.unwrap(), TransactionImportResult::Current);
assert_eq!(miner.pending_transactions().len(), 1);
assert_eq!(miner.ready_transactions(best_block).len(), 1);
assert_eq!(miner.ready_transactions(best_block, 0).len(), 1);
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 1);
assert_eq!(miner.pending_receipts(best_block).len(), 1);
// This method will let us know if pending block was created (before calling that method)
@ -1345,7 +1345,7 @@ mod tests {
// then
assert_eq!(res.unwrap(), TransactionImportResult::Current);
assert_eq!(miner.pending_transactions().len(), 1);
assert_eq!(miner.ready_transactions(best_block).len(), 0);
assert_eq!(miner.ready_transactions(best_block, 0).len(), 0);
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 0);
assert_eq!(miner.pending_receipts(best_block).len(), 0);
}
@ -1364,7 +1364,7 @@ mod tests {
assert_eq!(res.unwrap(), TransactionImportResult::Current);
assert_eq!(miner.pending_transactions().len(), 1);
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 0);
assert_eq!(miner.ready_transactions(best_block).len(), 0);
assert_eq!(miner.ready_transactions(best_block, 0).len(), 0);
assert_eq!(miner.pending_receipts(best_block).len(), 0);
// This method will let us know if pending block was created (before calling that method)
assert!(miner.prepare_work_sealing(&client));

View File

@ -154,7 +154,7 @@ pub trait MinerService : Send + Sync {
fn pending_transactions(&self) -> Vec<PendingTransaction>;
/// Get a list of all transactions that can go into the given block.
fn ready_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction>;
fn ready_transactions(&self, best_block: BlockNumber, best_block_timestamp: u64) -> Vec<PendingTransaction>;
/// Get a list of all future transactions.
fn future_transactions(&self) -> Vec<PendingTransaction>;

View File

@ -276,17 +276,17 @@ struct VerifiedTransaction {
origin: TransactionOrigin,
/// Insertion time
insertion_time: QueuingInstant,
/// Delay until specifid block.
min_block: Option<BlockNumber>,
/// Delay until specified condition is met.
condition: Option<Condition>,
}
impl VerifiedTransaction {
fn new(transaction: SignedTransaction, origin: TransactionOrigin, time: QueuingInstant, min_block: Option<BlockNumber>) -> Self {
fn new(transaction: SignedTransaction, origin: TransactionOrigin, time: QueuingInstant, condition: Option<Condition>) -> Self {
VerifiedTransaction {
transaction: transaction,
origin: origin,
insertion_time: time,
min_block: min_block,
condition: condition,
}
}
@ -666,14 +666,14 @@ impl TransactionQueue {
tx: SignedTransaction,
origin: TransactionOrigin,
time: QueuingInstant,
min_block: Option<BlockNumber>,
condition: Option<Condition>,
details_provider: &TransactionDetailsProvider,
) -> Result<TransactionImportResult, Error> {
if origin == TransactionOrigin::Local {
let hash = tx.hash();
let cloned_tx = tx.clone();
let result = self.add_internal(tx, origin, time, min_block, details_provider);
let result = self.add_internal(tx, origin, time, condition, details_provider);
match result {
Ok(TransactionImportResult::Current) => {
self.local_transactions.mark_pending(hash);
@ -694,7 +694,7 @@ impl TransactionQueue {
}
result
} else {
self.add_internal(tx, origin, time, min_block, details_provider)
self.add_internal(tx, origin, time, condition, details_provider)
}
}
@ -704,7 +704,7 @@ impl TransactionQueue {
tx: SignedTransaction,
origin: TransactionOrigin,
time: QueuingInstant,
min_block: Option<BlockNumber>,
condition: Option<Condition>,
details_provider: &TransactionDetailsProvider,
) -> Result<TransactionImportResult, Error> {
if origin != TransactionOrigin::Local && tx.gas_price < self.minimal_gas_price {
@ -815,7 +815,7 @@ impl TransactionQueue {
}
tx.check_low_s()?;
// No invalid transactions beyond this point.
let vtx = VerifiedTransaction::new(tx, origin, time, min_block);
let vtx = VerifiedTransaction::new(tx, origin, time, condition);
let r = self.import_tx(vtx, client_account.nonce).map_err(Error::Transaction);
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
r
@ -1068,11 +1068,11 @@ impl TransactionQueue {
/// Returns top transactions from the queue ordered by priority.
pub fn top_transactions(&self) -> Vec<SignedTransaction> {
self.top_transactions_at(BlockNumber::max_value())
self.top_transactions_at(BlockNumber::max_value(), u64::max_value())
}
fn filter_pending_transaction<F>(&self, best_block: BlockNumber, mut f: F)
fn filter_pending_transaction<F>(&self, best_block: BlockNumber, best_timestamp: u64, mut f: F)
where F: FnMut(&VerifiedTransaction) {
let mut delayed = HashSet::new();
@ -1082,7 +1082,12 @@ impl TransactionQueue {
if delayed.contains(&sender) {
continue;
}
if tx.min_block.unwrap_or(0) > best_block {
let delay = match tx.condition {
Some(Condition::Number(n)) => n > best_block,
Some(Condition::Timestamp(t)) => t > best_timestamp,
None => false,
};
if delay {
delayed.insert(sender);
continue;
}
@ -1091,16 +1096,16 @@ impl TransactionQueue {
}
/// Returns top transactions from the queue ordered by priority.
pub fn top_transactions_at(&self, best_block: BlockNumber) -> Vec<SignedTransaction> {
pub fn top_transactions_at(&self, best_block: BlockNumber, best_timestamp: u64) -> Vec<SignedTransaction> {
let mut r = Vec::new();
self.filter_pending_transaction(best_block, |tx| r.push(tx.transaction.clone()));
self.filter_pending_transaction(best_block, best_timestamp, |tx| r.push(tx.transaction.clone()));
r
}
/// Return all ready transactions.
pub fn pending_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction> {
pub fn pending_transactions(&self, best_block: BlockNumber, best_timestamp: u64) -> Vec<PendingTransaction> {
let mut r = Vec::new();
self.filter_pending_transaction(best_block, |tx| r.push(PendingTransaction::new(tx.transaction.clone(), tx.min_block)));
self.filter_pending_transaction(best_block, best_timestamp, |tx| r.push(PendingTransaction::new(tx.transaction.clone(), tx.condition.clone())));
r
}
@ -1109,7 +1114,7 @@ impl TransactionQueue {
self.future.by_priority
.iter()
.map(|t| self.by_hash.get(&t.hash).expect("All transactions in `current` and `future` are always included in `by_hash`"))
.map(|t| PendingTransaction { transaction: t.transaction.clone(), min_block: t.min_block })
.map(|t| PendingTransaction { transaction: t.transaction.clone(), condition: t.condition.clone() })
.collect()
}
@ -1382,7 +1387,7 @@ pub mod test {
use super::{TransactionSet, TransactionOrder, VerifiedTransaction};
use miner::local_transactions::LocalTransactionsList;
use client::TransactionImportResult;
use transaction::{SignedTransaction, Transaction, Action};
use transaction::{SignedTransaction, Transaction, Action, Condition};
pub struct DummyTransactionDetailsProvider {
account_details: AccountDetails,
@ -2178,15 +2183,15 @@ pub mod test {
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
// when
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, Some(1), &default_tx_provider()).unwrap();
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, Some(Condition::Number(1)), &default_tx_provider()).unwrap();
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_tx_provider()).unwrap();
// then
assert_eq!(res1, TransactionImportResult::Current);
assert_eq!(res2, TransactionImportResult::Current);
let top = txq.top_transactions_at(0);
let top = txq.top_transactions_at(0, 0);
assert_eq!(top.len(), 0);
let top = txq.top_transactions_at(1);
let top = txq.top_transactions_at(1, 0);
assert_eq!(top.len(), 2);
}

View File

@ -29,7 +29,7 @@ use spec::Spec;
use views::BlockView;
use util::stats::Histogram;
use ethkey::{KeyPair, Secret};
use transaction::{PendingTransaction, Transaction, Action};
use transaction::{PendingTransaction, Transaction, Action, Condition};
use miner::MinerService;
#[test]
@ -299,7 +299,7 @@ fn does_not_propagate_delayed_transactions() {
action: Action::Call(Address::default()),
value: 0.into(),
data: Vec::new(),
}.sign(secret, None), Some(2));
}.sign(secret, None), Some(Condition::Number(2)));
let tx1 = PendingTransaction::new(Transaction {
nonce: 1.into(),
gas_price: 0.into(),

View File

@ -34,6 +34,8 @@ pub struct BlockChainInfo {
pub best_block_hash: H256,
/// Best blockchain block number.
pub best_block_number: BlockNumber,
/// Best blockchain block timestamp.
pub best_block_timestamp: u64,
/// Best ancient block hash.
pub ancient_block_hash: Option<H256>,
/// Best ancient block number.

View File

@ -52,6 +52,16 @@ impl Decodable for Action {
}
}
/// Transaction activation condition.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", binary)]
pub enum Condition {
/// Valid at this block number or later.
Number(BlockNumber),
/// Valid at this unix time or later.
Timestamp(u64),
}
/// A set of information describing an externally-originating message call
/// or contract creation operation.
#[derive(Default, Debug, Clone, PartialEq, Eq)]
@ -448,16 +458,16 @@ impl Deref for LocalizedTransaction {
pub struct PendingTransaction {
/// Signed transaction data.
pub transaction: SignedTransaction,
/// To be activated at this block. `None` for immediately.
pub min_block: Option<BlockNumber>,
/// To be activated at this condition. `None` for immediately.
pub condition: Option<Condition>,
}
impl PendingTransaction {
/// Create a new pending transaction from signed transaction.
pub fn new(signed: SignedTransaction, min_block: Option<BlockNumber>) -> Self {
pub fn new(signed: SignedTransaction, condition: Option<Condition>) -> Self {
PendingTransaction {
transaction: signed,
min_block: min_block,
condition: condition,
}
}
}
@ -466,7 +476,7 @@ impl From<SignedTransaction> for PendingTransaction {
fn from(t: SignedTransaction) -> Self {
PendingTransaction {
transaction: t,
min_block: None,
condition: None,
}
}
}

View File

@ -18,7 +18,7 @@ tiny-keccak = "1.0"
docopt = { version = "0.6", optional = true }
time = "0.1.34"
lazy_static = "0.2"
itertools = "0.4"
itertools = "0.5"
parking_lot = "0.3"
ethcrypto = { path = "../ethcrypto" }
ethcore-util = { path = "../util" }

View File

@ -16,6 +16,7 @@
use std::collections::{BTreeMap, HashMap};
use std::mem;
use std::path::PathBuf;
use parking_lot::{Mutex, RwLock};
use crypto::KEY_ITERATIONS;
@ -164,8 +165,8 @@ impl SecretStore for EthStore {
self.store.update(account_ref, old, safe_account)
}
fn local_path(&self) -> String {
self.store.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new())
fn local_path(&self) -> PathBuf {
self.store.dir.path().cloned().unwrap_or_else(PathBuf::new)
}
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> {

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use ethkey::{Address, Message, Signature, Secret, Public};
use Error;
use json::Uuid;
@ -73,7 +74,7 @@ pub trait SecretStore: SimpleSecretStore {
fn set_name(&self, account: &StoreAccountRef, name: String) -> Result<(), Error>;
fn set_meta(&self, account: &StoreAccountRef, meta: String) -> Result<(), Error>;
fn local_path(&self) -> String;
fn local_path(&self) -> PathBuf;
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address>;
fn import_geth_accounts(&self, vault: SecretVaultRef, desired: Vec<Address>, testnet: bool) -> Result<Vec<StoreAccountRef>, Error>;
}

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.3.58",
"version": "0.3.62",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",

View File

@ -16,11 +16,12 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { isPlainObject } from 'lodash';
import { info, warn, error } from './helpers/log';
import { Dummy } from '../src/jsonrpc/helpers';
import interfaces from '../src/jsonrpc';
import rustMethods from './helpers/parsed-rpc-traits';
const ROOT_DIR = path.join(__dirname, '../docs');
@ -28,20 +29,13 @@ if (!fs.existsSync(ROOT_DIR)) {
fs.mkdirSync(ROOT_DIR);
}
// INFO Logging helper
function info (log) {
console.log(chalk.blue(`INFO:\t${log}`));
}
// WARN Logging helper
function warn (log) {
console.warn(chalk.yellow(`WARN:\t${log}`));
}
// ERROR Logging helper
function error (log) {
console.error(chalk.red(`ERROR:\t${log}`));
}
Object.keys(rustMethods).forEach((group) => {
Object.keys(rustMethods[group]).forEach((method) => {
if (interfaces[group] == null || interfaces[group][method] == null) {
error(`${group}_${method} is defined in Rust traits, but not in js/src/jsonrpc/interfaces`);
}
});
});
function printType (type) {
return type.print || `\`${type.name}\``;
@ -291,7 +285,8 @@ Object.keys(interfaces).sort().forEach((group) => {
Object.keys(spec).sort(methodComparator).forEach((iname) => {
const method = spec[iname];
const name = `${group.replace(/_.*$/, '')}_${iname}`;
const groupName = group.replace(/_.*$/, '');
const name = `${groupName}_${iname}`;
if (method.nodoc || method.deprecated) {
info(`Skipping ${name}: ${method.nodoc || 'Deprecated'}`);
@ -299,6 +294,10 @@ Object.keys(interfaces).sort().forEach((group) => {
return;
}
if (rustMethods[groupName] == null || rustMethods[groupName][iname] == null) {
error(`${name} is defined in js/src/jsonrpc/interfaces, but not in Rust traits`);
}
const desc = method.desc;
const params = buildParameters(method.params);
const returns = `- ${formatType(method.returns)}`;

32
js/scripts/helpers/log.js Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import chalk from 'chalk';
// INFO Logging helper
export function info (log) {
console.log(chalk.blue(`INFO:\t${log}`));
}
// WARN Logging helper
export function warn (log) {
console.warn(chalk.yellow(`WARN:\t${log}`));
}
// ERROR Logging helper
export function error (log) {
console.error(chalk.red(`ERROR:\t${log}`));
}

View File

@ -0,0 +1,81 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import fs from 'fs';
import path from 'path';
// ```js
// rustMethods['eth']['call'] === true
// ```
const rustMethods = {};
export default rustMethods;
// Get a list of JSON-RPC from Rust trait source code
function parseMethodsFromRust (source) {
// Matching the custom `rpc` attribute with it's doc comment
const attributePattern = /((?:\s*\/\/\/.*$)*)\s*#\[rpc\(([^)]+)\)]/gm;
const commentPattern = /\s*\/\/\/\s*/g;
const separatorPattern = /\s*,\s*/g;
const assignPattern = /([\S]+)\s*=\s*"([^"]*)"/;
const ignorePattern = /@(ignore|deprecated|unimplemented|alias)\b/i;
const methods = [];
source.toString().replace(attributePattern, (match, comment, props) => {
comment = comment.replace(commentPattern, '\n').trim();
// Skip deprecated methods
if (ignorePattern.test(comment)) {
return match;
}
props.split(separatorPattern).forEach((prop) => {
const [, key, value] = prop.split(assignPattern) || [];
if (key === 'name' && value != null) {
methods.push(value);
}
});
return match;
});
return methods;
}
// Get a list of all JSON-RPC methods from all defined traits
function getMethodsFromRustTraits () {
const traitsDir = path.join(__dirname, '../../../rpc/src/v1/traits');
return fs.readdirSync(traitsDir)
.filter((name) => name !== 'mod.rs' && /\.rs$/.test(name))
.map((name) => fs.readFileSync(path.join(traitsDir, name)))
.map(parseMethodsFromRust)
.reduce((a, b) => a.concat(b));
}
getMethodsFromRustTraits().sort().forEach((method) => {
const [group, name] = method.split('_');
// Skip methods with malformed names
if (group == null || name == null) {
return;
}
rustMethods[group] = rustMethods[group] || {};
rustMethods[group][name] = true;
});

View File

@ -23,6 +23,7 @@ export default class Eth {
this._started = false;
this._lastBlock = new BigNumber(-1);
this._pollTimerId = null;
}
get isStarted () {
@ -37,7 +38,7 @@ export default class Eth {
_blockNumber = () => {
const nextTimeout = (timeout = 1000) => {
setTimeout(() => {
this._pollTimerId = setTimeout(() => {
this._blockNumber();
}, timeout);
};
@ -57,6 +58,6 @@ export default class Eth {
nextTimeout();
})
.catch(nextTimeout);
.catch(() => nextTimeout());
}
}

View File

@ -20,6 +20,9 @@ export default class Personal {
this._api = api;
this._updateSubscriptions = updateSubscriptions;
this._started = false;
this._lastDefaultAccount = '0x0';
this._pollTimerId = null;
}
get isStarted () {
@ -37,12 +40,35 @@ export default class Personal {
]);
}
_defaultAccount = () => {
// FIXME: Because of the different API instances, the "wait for valid changes" approach
// doesn't work. Since the defaultAccount is critical to operation, we poll in exactly
// same way we do in ../eth (ala same as eth_blockNumber) and update. This should be moved
// to pub-sub as it becomes available
_defaultAccount = (timerDisabled = false) => {
const nextTimeout = (timeout = 1000) => {
if (!timerDisabled) {
this._pollTimerId = setTimeout(() => {
this._defaultAccount();
}, timeout);
}
};
if (!this._api.transport.isConnected) {
nextTimeout(500);
return;
}
return this._api.parity
.defaultAccount()
.then((defaultAccount) => {
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
});
if (this._lastDefaultAccount !== defaultAccount) {
this._lastDefaultAccount = defaultAccount;
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
}
nextTimeout();
})
.catch(() => nextTimeout());
}
_listAccounts = () => {
@ -54,14 +80,20 @@ export default class Personal {
}
_accountsInfo = () => {
return Promise
.all([
this._api.parity.accountsInfo(),
this._api.parity.allAccountsInfo()
])
.then(([info, allInfo]) => {
return this._api.parity
.accountsInfo()
.then((info) => {
this._updateSubscriptions('parity_accountsInfo', null, info);
this._updateSubscriptions('parity_allAccountsInfo', null, allInfo);
return this._api.parity
.allAccountsInfo()
.catch(() => {
// NOTE: This fails on non-secure APIs, swallow error
return {};
})
.then((allInfo) => {
this._updateSubscriptions('parity_allAccountsInfo', null, allInfo);
});
});
}
@ -89,7 +121,7 @@ export default class Personal {
case 'parity_setDappsAddresses':
case 'parity_setNewDappsWhitelist':
this._defaultAccount();
this._defaultAccount(true);
return;
}
});

View File

@ -36,6 +36,9 @@ function stubApi (accounts, info) {
return {
_calls,
transport: {
isConnected: true
},
parity: {
accountsInfo: () => {
const stub = sinon.stub().resolves(info || TEST_INFO)();

View File

@ -1 +1 @@
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]

View File

@ -20,23 +20,23 @@ export const checkIfVerified = (contract, account) => {
return contract.instance.certified.call({}, [account]);
};
export const checkIfRequested = (contract, account) => {
export const findLastRequested = (contract, account) => {
let subId = null;
let resolved = false;
return new Promise((resolve, reject) => {
contract
.subscribe('Requested', {
fromBlock: 0, toBlock: 'pending'
fromBlock: 0,
toBlock: 'pending',
limit: 1,
topics: [account]
}, (err, logs) => {
if (err) {
return reject(err);
}
const e = logs.find((l) => {
return l.type === 'mined' && l.params.who && l.params.who.value === account;
});
resolve(e ? e.transactionHash : false);
resolve(logs[0] || null);
resolved = true;
if (subId) {

View File

@ -45,7 +45,7 @@ export default class Application extends Component {
}
componentDidMount () {
this.attachInstance();
return this.attachInstance();
}
render () {
@ -80,12 +80,12 @@ export default class Application extends Component {
}
attachInstance () {
Promise
return Promise
.all([
attachInstances(),
api.parity.accountsInfo()
api.parity.accountsInfo(),
attachInstances()
])
.then(([{ managerInstance, registryInstance, tokenregInstance }, accountsInfo]) => {
.then(([accountsInfo, { managerInstance, registryInstance, tokenregInstance }]) => {
accountsInfo = accountsInfo || {};
this.setState({
loading: false,

View File

@ -17,7 +17,6 @@
import React, { Component, PropTypes } from 'react';
import { api } from '../../parity';
import AddressSelect from '../../AddressSelect';
import Container from '../../Container';
import styles from './deployment.css';
@ -122,36 +121,13 @@ export default class Deployment extends Component {
}
renderForm () {
const { accounts } = this.context;
const { baseText, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const hasError = !!(nameError || tlaError || totalSupplyError);
const error = `${styles.input} ${styles.error}`;
const addresses = Object.keys(accounts);
// <div className={ styles.input }>
// <label>global registration</label>
// <select onChange={ this.onChangeRegistrar }>
// <option value='no'>No, only for me</option>
// <option value='yes'>Yes, for everybody</option>
// </select>
// <div className={ styles.hint }>
// register on network (fee: { globalFeeText }ETH)
// </div>
// </div>
return (
<Container>
<div className={ styles.form }>
<div className={ styles.input }>
<label>deployment account</label>
<AddressSelect
addresses={ addresses }
onChange={ this.onChangeFrom }
/>
<div className={ styles.hint }>
the owner account to deploy from
</div>
</div>
<div className={ nameError ? error : styles.input }>
<label>token name</label>
<input
@ -206,12 +182,6 @@ export default class Deployment extends Component {
);
}
onChangeFrom = (event) => {
const fromAddress = event.target.value;
this.setState({ fromAddress });
}
onChangeName = (event) => {
const name = event.target.value;
const nameError = name && (name.length > 2) && (name.length < 32)
@ -271,7 +241,7 @@ export default class Deployment extends Component {
onDeploy = () => {
const { managerInstance, registryInstance, tokenregInstance } = this.context;
const { base, deployBusy, fromAddress, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const { base, deployBusy, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const hasError = !!(nameError || tlaError || totalSupplyError);
if (hasError || deployBusy) {
@ -281,14 +251,18 @@ export default class Deployment extends Component {
const tokenreg = (globalReg ? tokenregInstance : registryInstance).address;
const values = [base.mul(totalSupply), tla, name, tokenreg];
const options = {
from: fromAddress,
value: globalReg ? globalFee : 0
};
this.setState({ deployBusy: true, deployState: 'Estimating gas for the transaction' });
managerInstance
.deploy.estimateGas(options, values)
return api.parity
.defaultAccount()
.then((defaultAddress) => {
options.from = defaultAddress;
return managerInstance.deploy.estimateGas(options, values);
})
.then((gas) => {
this.setState({ deployState: 'Gas estimated, Posting transaction to the network' });

View File

@ -25,6 +25,8 @@ let registryInstance;
const registries = {};
const subscriptions = {};
let defaultSubscriptionId;
let nextSubscriptionId = 1000;
let isTest = false;
@ -65,6 +67,20 @@ export function unsubscribeEvents (subscriptionId) {
delete subscriptions[subscriptionId];
}
export function subscribeDefaultAddress (callback) {
return api
.subscribe('parity_defaultAccount', callback)
.then((subscriptionId) => {
defaultSubscriptionId = subscriptionId;
return defaultSubscriptionId;
});
}
export function unsubscribeDefaultAddress () {
return api.unsubscribe(defaultSubscriptionId);
}
function pollEvents () {
const loop = Object.values(subscriptions);
const timeout = () => setTimeout(pollEvents, 1000);

View File

@ -17,10 +17,9 @@
import React, { Component } from 'react';
import { api } from '../parity';
import { attachInterface } from '../services';
import { attachInterface, subscribeDefaultAddress, unsubscribeDefaultAddress } from '../services';
import Button from '../Button';
import Events from '../Events';
import IdentityIcon from '../IdentityIcon';
import Loading from '../Loading';
import styles from './application.css';
@ -32,7 +31,7 @@ let nextEventId = 0;
export default class Application extends Component {
state = {
fromAddress: null,
defaultAddress: null,
loading: true,
url: '',
urlError: null,
@ -47,19 +46,32 @@ export default class Application extends Component {
registerType: 'file',
repo: '',
repoError: null,
subscriptionId: null,
events: {},
eventIds: []
}
componentDidMount () {
attachInterface()
.then((state) => {
this.setState(state, () => {
this.setState({ loading: false });
});
return Promise
.all([
attachInterface(),
subscribeDefaultAddress((error, defaultAddress) => {
if (!error) {
this.setState({ defaultAddress });
}
})
])
.then(([state]) => {
this.setState(Object.assign({}, state, {
loading: false
}));
});
}
componentWillUnmount () {
return unsubscribeDefaultAddress();
}
render () {
const { loading } = this.state;
@ -75,12 +87,14 @@ export default class Application extends Component {
}
renderPage () {
const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
const { defaultAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
let hashClass = null;
if (contentHashError) {
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
hashClass = contentHashOwner !== defaultAddress
? styles.hashError
: styles.hashWarning;
} else if (contentHash) {
hashClass = styles.hashOk;
}
@ -166,20 +180,13 @@ export default class Application extends Component {
}
renderButtons () {
const { accounts, fromAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
const account = accounts[fromAddress];
const { defaultAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
return (
<div className={ styles.buttons }>
<div className={ styles.addressSelect }>
<Button invert onClick={ this.onSelectFromAddress }>
<IdentityIcon address={ account.address } />
<div>{ account.name || account.address }</div>
</Button>
</div>
<Button
onClick={ this.onClickRegister }
disabled={ (contentHashError && contentHashOwner !== fromAddress) || urlError || repoError || commitError }
disabled={ (contentHashError && contentHashOwner !== defaultAddress) || urlError || repoError || commitError }
>register url</Button>
</div>
);
@ -294,11 +301,11 @@ export default class Application extends Component {
}
onClickRegister = () => {
const { commit, commitError, contentHashError, contentHashOwner, fromAddress, url, urlError, registerType, repo, repoError } = this.state;
const { defaultAddress, commit, commitError, contentHashError, contentHashOwner, url, urlError, registerType, repo, repoError } = this.state;
// TODO: No errors are currently set, validation to be expanded and added for each
// field (query is fast to pick up the issues, so not burning atm)
if ((contentHashError && contentHashOwner !== fromAddress) || repoError || urlError || commitError) {
if ((contentHashError && contentHashOwner !== defaultAddress) || repoError || urlError || commitError) {
return;
}
@ -368,13 +375,15 @@ export default class Application extends Component {
}
registerContent (contentRepo, contentCommit) {
const { contentHash, fromAddress, instance } = this.state;
const { defaultAddress, contentHash, instance } = this.state;
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
contentCommit = contentCommit.substr(0, 2) === '0x'
? contentCommit
: `0x${contentCommit}`;
const eventId = nextEventId++;
const values = [contentHash, contentRepo, contentCommit];
const options = { from: fromAddress };
const options = { from: defaultAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
@ -383,7 +392,7 @@ export default class Application extends Component {
contentHash,
contentRepo,
contentCommit,
fromAddress,
defaultAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
@ -421,11 +430,11 @@ export default class Application extends Component {
}
registerUrl (contentUrl) {
const { contentHash, fromAddress, instance } = this.state;
const { contentHash, defaultAddress, instance } = this.state;
const eventId = nextEventId++;
const values = [contentHash, contentUrl];
const options = { from: fromAddress };
const options = { from: defaultAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
@ -433,7 +442,7 @@ export default class Application extends Component {
[eventId]: {
contentHash,
contentUrl,
fromAddress,
defaultAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
@ -470,25 +479,6 @@ export default class Application extends Component {
);
}
onSelectFromAddress = () => {
const { accounts, fromAddress } = this.state;
const addresses = Object.keys(accounts);
let index = 0;
addresses.forEach((address, _index) => {
if (address === fromAddress) {
index = _index;
}
});
index++;
if (index >= addresses.length) {
index = 0;
}
this.setState({ fromAddress: addresses[index] });
}
lookupHash (url) {
const { instance } = this.state;

View File

@ -17,48 +17,44 @@
import * as abis from '~/contracts/abi';
import { api } from './parity';
let defaultSubscriptionId;
export function attachInterface () {
return api.parity
.registryAddress()
.then((registryAddress) => {
console.log(`the registry was found at ${registryAddress}`);
const registry = api.newContract(abis.registry, registryAddress).instance;
return Promise
.all([
registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']),
api.parity.accountsInfo()
]);
return api
.newContract(abis.registry, registryAddress).instance
.getAddress.call({}, [api.util.sha3('githubhint'), 'A']);
})
.then(([address, accountsInfo]) => {
.then((address) => {
console.log(`githubhint was found at ${address}`);
const contract = api.newContract(abis.githubhint, address);
const accounts = Object
.keys(accountsInfo)
.reduce((obj, address) => {
const account = accountsInfo[address];
return Object.assign(obj, {
[address]: {
address,
name: account.name
}
});
}, {});
const fromAddress = Object.keys(accounts)[0];
return {
accounts,
address,
accountsInfo,
contract,
instance: contract.instance,
fromAddress
instance: contract.instance
};
})
.catch((error) => {
console.error('attachInterface', error);
});
}
export function subscribeDefaultAddress (callback) {
return api
.subscribe('parity_defaultAccount', callback)
.then((subscriptionId) => {
defaultSubscriptionId = subscriptionId;
return defaultSubscriptionId;
});
}
export function unsubscribeDefaultAddress () {
return api.unsubscribe(defaultSubscriptionId);
}

View File

@ -22,7 +22,7 @@ import { api } from '../parity';
import styles from './transaction.css';
import IdentityIcon from '../../githubhint/IdentityIcon';
import IdentityIcon from '../IdentityIcon';
class BaseTransaction extends Component {
shortHash (hash) {

View File

@ -82,6 +82,7 @@ export const ownerLookup = (name) => (dispatch, getState) => {
return;
}
name = name.toLowerCase();
dispatch(ownerLookupStart(name));
return getOwner(contract, name)

View File

@ -30,7 +30,6 @@ export default class Application extends Component {
state = {
accounts: {},
address: null,
fromAddress: null,
accountsInfo: {},
blockNumber: new BigNumber(0),
contract: null,
@ -41,11 +40,9 @@ export default class Application extends Component {
}
componentDidMount () {
attachInterface()
return attachInterface()
.then((state) => {
this.setState(state, () => {
this.setState({ loading: false });
});
this.setState(Object.assign({}, state, { loading: false }));
return attachBlockNumber(state.instance, (state) => {
this.setState(state);
@ -86,17 +83,14 @@ export default class Application extends Component {
}
renderImport () {
const { accounts, fromAddress, instance, showImport } = this.state;
const { instance, showImport } = this.state;
if (showImport) {
return (
<Import
accounts={ accounts }
fromAddress={ fromAddress }
instance={ instance }
visible={ showImport }
onClose={ this.toggleImport }
onSetFromAddress={ this.setFromAddress }
/>
);
}
@ -124,10 +118,4 @@ export default class Application extends Component {
showImport: !this.state.showImport
});
}
setFromAddress = (fromAddress) => {
this.setState({
fromAddress
});
}
}

View File

@ -19,18 +19,14 @@ import React, { Component, PropTypes } from 'react';
import { api } from '../parity';
import { callRegister, postRegister } from '../services';
import Button from '../Button';
import IdentityIcon from '../IdentityIcon';
import styles from './import.css';
export default class Import extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
fromAddress: PropTypes.string.isRequired,
instance: PropTypes.object.isRequired,
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSetFromAddress: PropTypes.func.isRequired
onClose: PropTypes.func.isRequired
}
state = {
@ -83,21 +79,12 @@ export default class Import extends Component {
}
renderRegister () {
const { accounts, fromAddress } = this.props;
const account = accounts[fromAddress];
const count = this.countFunctions();
let buttons = null;
if (count) {
buttons = (
<div className={ styles.buttonrow }>
<div className={ styles.addressSelect }>
<Button invert onClick={ this.onSelectFromAddress }>
<IdentityIcon address={ account.address } />
<div>{ account.name || account.address }</div>
</Button>
</div>
<Button onClick={ this.onRegister }>
register functions
</Button>
@ -197,15 +184,15 @@ export default class Import extends Component {
}
onRegister = () => {
const { instance, fromAddress, onClose } = this.props;
const { instance, onClose } = this.props;
const { functions, fnstate } = this.state;
Promise
return Promise
.all(
functions
.filter((fn) => !fn.constant)
.filter((fn) => fnstate[fn.signature] === 'fntodo')
.map((fn) => postRegister(instance, fn.id, { from: fromAddress }))
.map((fn) => postRegister(instance, fn.id, {}))
)
.then(() => {
onClose();
@ -214,23 +201,4 @@ export default class Import extends Component {
console.error('onRegister', error);
});
}
onSelectFromAddress = () => {
const { accounts, fromAddress, onSetFromAddress } = this.props;
const addresses = Object.keys(accounts);
let index = 0;
addresses.forEach((address, _index) => {
if (address === fromAddress) {
index = _index;
}
});
index++;
if (index >= addresses.length) {
index = 0;
}
onSetFromAddress(addresses[index]);
}
}

View File

@ -166,8 +166,13 @@ export function callRegister (instance, id, options = {}) {
}
export function postRegister (instance, id, options = {}) {
return instance.register
.estimateGas(options, [id])
return api.parity
.defaultAccount()
.then((defaultAddress) => {
options.from = defaultAddress;
return instance.register.estimateGas(options, [id]);
})
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
console.log('postRegister', `gas estimated at ${gas.toFormat(0)}, setting to ${gas.mul(1.2).toFormat(0)}`);

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/>.
import fs from 'fs';
import path from 'path';
import interfaces from './';
import * as customTypes from './types';
@ -27,91 +25,13 @@ function verifyType (obj) {
}
}
// Get a list of JSON-RPC from Rust trait source code
function parseMethodsFromRust (source) {
// Matching the custom `rpc` attribute with it's doc comment
const attributePattern = /((?:\s*\/\/\/.*$)*)\s*#\[rpc\(([^)]+)\)]/gm;
const commentPattern = /\s*\/\/\/\s*/g;
const separatorPattern = /\s*,\s*/g;
const assignPattern = /([\S]+)\s*=\s*"([^"]*)"/;
const ignorePattern = /@(ignore|deprecated|unimplemented|alias)\b/i;
const methods = [];
source.toString().replace(attributePattern, (match, comment, props) => {
comment = comment.replace(commentPattern, '\n').trim();
// Skip deprecated methods
if (ignorePattern.test(comment)) {
return match;
}
props.split(separatorPattern).forEach((prop) => {
const [, key, value] = prop.split(assignPattern) || [];
if (key === 'name' && value != null) {
methods.push(value);
}
});
return match;
});
return methods;
}
// Get a list of all JSON-RPC methods from all defined traits
function getMethodsFromRustTraits () {
const traitsDir = path.join(__dirname, '../../../rpc/src/v1/traits');
return fs.readdirSync(traitsDir)
.filter((name) => name !== 'mod.rs' && /\.rs$/.test(name))
.map((name) => fs.readFileSync(path.join(traitsDir, name)))
.map(parseMethodsFromRust)
.reduce((a, b) => a.concat(b));
}
const rustMethods = {};
getMethodsFromRustTraits().sort().forEach((method) => {
const [group, name] = method.split('_');
// Skip methods with malformed names
if (group == null || name == null) {
return;
}
rustMethods[group] = rustMethods[group] || {};
rustMethods[group][name] = true;
});
describe('jsonrpc/interfaces', () => {
describe('Rust trait methods', () => {
Object.keys(rustMethods).forEach((group) => {
describe(group, () => {
Object.keys(rustMethods[group]).forEach((name) => {
describe(name, () => {
it('has a defined JS interface', () => {
expect(interfaces[group][name]).to.exist;
});
});
});
});
});
});
Object.keys(interfaces).forEach((group) => {
describe(group, () => {
Object.keys(interfaces[group]).forEach((name) => {
const method = interfaces[group][name];
describe(name, () => {
if (!method.nodoc) {
it('is present in Rust codebase', () => {
expect(rustMethods[group][name]).to.exist;
});
}
it('has the correct interface', () => {
expect(method.desc).to.be.a('string');
expect(method.params).to.be.an('array');

View File

@ -15,15 +15,24 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.modal {
flex-direction: column;
}
.container {
margin-top: 1.5em;
overflow-y: auto;
}
.description {
margin-top: .5em !important;
}
.list {
margin-bottom: 1.5em;
.background {
background: rgba(255, 255, 255, 0.2);
margin: 0 -1.5em;
padding: 0.5em 1.5em;
padding: 0.5em 0;
}
.header {
@ -37,3 +46,26 @@
opacity: 0.75;
}
}
.selectIcon {
position: absolute;
right: 0.5em;
top: 0.5em;
}
.selected,
.unselected {
position: relative;
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
.selectIcon {
opacity: 0.15;
}
}
.selected {
background: rgba(255, 255, 255, 0.15) !important;
}

View File

@ -14,14 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal, Button } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import { ContainerTitle, DappCard, Portal, SectionList } from '~/ui';
import { CheckIcon } from '~/ui/Icons';
import styles from './addDapps.css';
@ -39,71 +37,63 @@ export default class AddDapps extends Component {
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.add.button.done'
defaultMessage='Done'
/>
}
onClick={ store.closeModal }
/>
] }
compact
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications'
/>
}
visible
<Portal
className={ styles.modal }
onClose={ store.closeModal }
open
>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal,
<ContainerTitle
title={
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available'
/>,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.'
id='dapps.add.label'
defaultMessage='visible applications'
/>
)
}
{
this.renderList(store.sortedBuiltin,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity'
/>,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.'
/>
)
}
{
this.renderList(store.sortedNetwork,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network'
/>,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.'
/>
)
}
</Modal>
}
/>
<div className={ styles.container }>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal, store.displayApps,
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available'
/>,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.'
/>
)
}
{
this.renderList(store.sortedBuiltin, store.displayApps,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity'
/>,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.'
/>
)
}
{
this.renderList(store.sortedNetwork, store.displayApps,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network'
/>,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.'
/>
)
}
</div>
</Portal>
);
}
renderList (items, header, byline) {
renderList (items, visibleItems, header, byline) {
if (!items || !items.length) {
return null;
}
@ -114,41 +104,40 @@ export default class AddDapps extends Component {
<div className={ styles.header }>{ header }</div>
<div className={ styles.byline }>{ byline }</div>
</div>
<List>
{ items.map(this.renderApp) }
</List>
<SectionList
items={ items }
noStretch
renderItem={ this.renderApp }
/>
</div>
);
}
renderApp = (app) => {
const { store } = this.props;
const isHidden = !store.displayApps[app.id].visible;
const isVisible = store.displayApps[app.id].visible;
const onCheck = () => {
if (isHidden) {
store.showApp(app.id);
} else {
const onClick = () => {
if (isVisible) {
store.hideApp(app.id);
} else {
store.showApp(app.id);
}
};
return (
<ListItem
<DappCard
app={ app }
className={
isVisible
? styles.selected
: styles.unselected
}
key={ app.id }
leftCheckbox={
<Checkbox
checked={ !isHidden }
onCheck={ onCheck }
/>
}
primaryText={ app.name }
secondaryText={
<div className={ styles.description }>
{ app.description }
</div>
}
/>
onClick={ onClick }
>
<CheckIcon className={ styles.selectIcon } />
</DappCard>
);
}
}

View File

@ -33,13 +33,13 @@ describe('modals/AddDapps', () => {
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
renderShallow({ modalOpen: false }).find('Portal')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true }).find('Connect(Modal)')
renderShallow({ modalOpen: true }).find('Portal')
).to.have.length(1);
});
});

View File

@ -30,6 +30,10 @@
margin-left: .5em;
}
.field {
margin-bottom: .5em;
}
.terms {
line-height: 1.3;
opacity: .7;

View File

@ -31,19 +31,22 @@ import emailTermsOfService from '~/3rdparty/email-verification/terms-of-service'
import { howSMSVerificationWorks, howEmailVerificationWorks } from '../how-it-works';
import styles from './gatherData.css';
const boolOfError = PropTypes.oneOfType([ PropTypes.bool, PropTypes.instanceOf(Error) ]);
export default class GatherData extends Component {
static propTypes = {
fee: React.PropTypes.instanceOf(BigNumber),
fields: PropTypes.array.isRequired,
hasRequested: nullableProptype(PropTypes.bool.isRequired),
accountHasRequested: nullableProptype(PropTypes.bool.isRequired),
isServerRunning: nullableProptype(PropTypes.bool.isRequired),
isVerified: nullableProptype(PropTypes.bool.isRequired),
isAbleToRequest: nullableProptype(boolOfError.isRequired),
accountIsVerified: nullableProptype(PropTypes.bool.isRequired),
method: PropTypes.string.isRequired,
setConsentGiven: PropTypes.func.isRequired
}
render () {
const { method, isVerified } = this.props;
const { method, accountIsVerified } = this.props;
const termsOfService = method === 'email' ? emailTermsOfService : smsTermsOfService;
const howItWorks = method === 'email' ? howEmailVerificationWorks : howSMSVerificationWorks;
@ -55,6 +58,7 @@ export default class GatherData extends Component {
{ this.renderCertified() }
{ this.renderRequested() }
{ this.renderFields() }
{ this.renderIfAbleToRequest() }
<Checkbox
className={ styles.spacing }
label={
@ -63,7 +67,7 @@ export default class GatherData extends Component {
defaultMessage='I agree to the terms and conditions below.'
/>
}
disabled={ isVerified }
disabled={ accountIsVerified }
onCheck={ this.consentOnChange }
/>
<div className={ styles.terms }>{ termsOfService }</div>
@ -145,27 +149,27 @@ export default class GatherData extends Component {
}
renderCertified () {
const { isVerified } = this.props;
const { accountIsVerified } = this.props;
if (isVerified) {
if (accountIsVerified) {
return (
<div className={ styles.container }>
<ErrorIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.true'
id='ui.verification.gatherData.accountIsVerified.true'
defaultMessage='Your account is already verified.'
/>
</p>
</div>
);
} else if (isVerified === false) {
} else if (accountIsVerified === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.false'
id='ui.verification.gatherData.accountIsVerified.false'
defaultMessage='Your account is not verified yet.'
/>
</p>
@ -175,7 +179,7 @@ export default class GatherData extends Component {
return (
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.pending'
id='ui.verification.gatherData.accountIsVerified.pending'
defaultMessage='Checking if your account is verified…'
/>
</p>
@ -183,33 +187,33 @@ export default class GatherData extends Component {
}
renderRequested () {
const { isVerified, hasRequested } = this.props;
const { accountIsVerified, accountHasRequested } = this.props;
// If the account is verified, don't show that it has requested verification.
if (isVerified) {
if (accountIsVerified) {
return null;
}
if (hasRequested) {
if (accountHasRequested) {
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.true'
defaultMessage='You already requested verification.'
id='ui.verification.gatherData.accountHasRequested.true'
defaultMessage='You already requested verification from this account.'
/>
</p>
</div>
);
} else if (hasRequested === false) {
} else if (accountHasRequested === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.false'
defaultMessage='You did not request verification yet.'
id='ui.verification.gatherData.accountHasRequested.false'
defaultMessage='You did not request verification from this account yet.'
/>
</p>
</div>
@ -218,7 +222,7 @@ export default class GatherData extends Component {
return (
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.pending'
id='ui.verification.gatherData.accountHasRequested.pending'
defaultMessage='Checking if you requested verification…'
/>
</p>
@ -226,7 +230,7 @@ export default class GatherData extends Component {
}
renderFields () {
const { isVerified, fields } = this.props;
const { accountIsVerified, fields } = this.props;
const rendered = fields.map((field) => {
const onChange = (_, v) => {
@ -236,11 +240,12 @@ export default class GatherData extends Component {
return (
<Input
className={ styles.field }
key={ field.key }
label={ field.label }
hint={ field.hint }
error={ field.error }
disabled={ isVerified }
disabled={ accountIsVerified }
onChange={ onChange }
onSubmit={ onSubmit }
/>
@ -250,6 +255,36 @@ export default class GatherData extends Component {
return (<div>{rendered}</div>);
}
renderIfAbleToRequest () {
const { accountIsVerified, isAbleToRequest } = this.props;
// If the account is verified, don't show a warning.
// If the client is able to send the request, don't show a warning
if (accountIsVerified || isAbleToRequest === true) {
return null;
}
if (isAbleToRequest === null) {
return (
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isAbleToRequest.pending'
defaultMessage='Validating your input…'
/>
</p>
);
} else if (isAbleToRequest) {
return (
<div className={ styles.container }>
<ErrorIcon />
<p className={ styles.message }>
{ isAbleToRequest.message }
</p>
</div>
);
}
}
consentOnChange = (_, consentGiven) => {
this.props.setConsentGiven(consentGiven);
}

View File

@ -16,6 +16,7 @@
import { observable, computed, action } from 'mobx';
import { sha3 } from '~/api/util/sha3';
import { bytesToHex } from '~/api/util/format';
import EmailVerificationABI from '~/contracts/abi/email-verification.json';
import VerificationStore, {
@ -23,6 +24,8 @@ import VerificationStore, {
} from './store';
import { isServerRunning, hasReceivedCode, postToServer } from '~/3rdparty/email-verification';
const ZERO20 = '0x0000000000000000000000000000000000000000';
// name in the `BadgeReg.sol` contract
const EMAIL_VERIFICATION = 'emailverification';
@ -44,9 +47,9 @@ export default class EmailVerificationStore extends VerificationStore {
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
case QUERY_DATA:
return this.isEmailValid && this.consentGiven;
return this.isEmailValid && this.consentGiven && this.isAbleToRequest === true;
case QUERY_CODE:
return this.requestTx && this.isCodeValid === true;
case POSTED_CONFIRMATION:
@ -68,8 +71,53 @@ export default class EmailVerificationStore extends VerificationStore {
return hasReceivedCode(this.email, this.account, this.isTestnet);
}
// If the email has already been used for verification of another account,
// we prevent the user from wasting ETH to request another verification.
@action setIfAbleToRequest = () => {
const { isEmailValid } = this;
if (!isEmailValid) {
this.isAbleToRequest = true;
return;
}
const { contract, email } = this;
const emailHash = sha3.text(email);
this.isAbleToRequest = null;
contract
.instance.reverse
.call({}, [ emailHash ])
.then((address) => {
if (address === ZERO20) {
this.isAbleToRequest = true;
} else {
this.isAbleToRequest = new Error('Another account has been verified using this e-mail.');
}
})
.catch((err) => {
this.error = 'Failed to check if able to send request: ' + err.message;
});
}
// Determine the values relevant for checking if the last request contains
// the same data as the current one.
requestValues = () => [ sha3.text(this.email) ]
shallSkipRequest = (currentValues) => {
const { accountHasRequested } = this;
const lastRequest = this.lastRequestValues;
if (!accountHasRequested) {
return Promise.resolve(false);
}
// If the last email verification `request` for the selected address contains
// the same email as the current one, don't send another request to save ETH.
const skip = currentValues[0] === bytesToHex(lastRequest.emailHash.value);
return Promise.resolve(skip);
}
@action setEmail = (email) => {
this.email = email;
}

View File

@ -43,7 +43,7 @@ export default class SMSVerificationStore extends VerificationStore {
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
case QUERY_DATA:
return this.isNumberValid && this.consentGiven;
case QUERY_CODE:
@ -67,6 +67,18 @@ export default class SMSVerificationStore extends VerificationStore {
return hasReceivedCode(this.number, this.account, this.isTestnet);
}
// SMS verification events don't contain the phone number, so we will have to
// send a new request every single time. See below.
@action setIfAbleToRequest = () => {
this.isAbleToRequest = true;
}
// SMS verification `request` & `confirm` transactions and events don't contain the
// phone number, so we will have to send a new request every single time. This may
// cost the user more money, but given that it fails otherwise, it seems like a
// reasonable tradeoff.
shallSkipRequest = () => Promise.resolve(false)
@action setNumber = (number) => {
this.number = number;
}

View File

@ -19,7 +19,7 @@ import { sha3 } from '~/api/util/sha3';
import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/verification';
import { checkIfVerified, findLastRequested, awaitPuzzle } from '~/contracts/verification';
import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
export const LOADING = 'fetching-contract';
@ -38,8 +38,10 @@ export default class VerificationStore {
@observable contract = null;
@observable fee = null;
@observable isVerified = null;
@observable hasRequested = null;
@observable accountIsVerified = null;
@observable accountHasRequested = null;
@observable isAbleToRequest = null;
@observable lastRequestValues = null;
@observable isServerRunning = null;
@observable consentGiven = false;
@observable requestTx = null;
@ -68,6 +70,14 @@ export default class VerificationStore {
console.error('verification: ' + this.error);
}
});
autorun(() => {
if (this.step !== QUERY_DATA) {
return;
}
this.setIfAbleToRequest();
});
}
@action load = () => {
@ -91,19 +101,20 @@ export default class VerificationStore {
this.error = 'Failed to fetch the fee: ' + err.message;
});
const isVerified = checkIfVerified(contract, account)
.then((isVerified) => {
this.isVerified = isVerified;
const accountIsVerified = checkIfVerified(contract, account)
.then((accountIsVerified) => {
this.accountIsVerified = accountIsVerified;
})
.catch((err) => {
this.error = 'Failed to check if verified: ' + err.message;
});
const hasRequested = checkIfRequested(contract, account)
.then((txHash) => {
this.hasRequested = !!txHash;
if (txHash) {
this.requestTx = txHash;
const accountHasRequested = findLastRequested(contract, account)
.then((log) => {
this.accountHasRequested = !!log;
if (log) {
this.lastRequestValues = log.params;
this.requestTx = log.transactionHash;
}
})
.catch((err) => {
@ -111,7 +122,7 @@ export default class VerificationStore {
});
Promise
.all([ isServerRunning, fee, isVerified, hasRequested ])
.all([ isServerRunning, fee, accountIsVerified, accountHasRequested ])
.then(() => {
this.step = QUERY_DATA;
});
@ -150,40 +161,41 @@ export default class VerificationStore {
requestValues = () => []
@action sendRequest = () => {
const { api, account, contract, fee, hasRequested } = this;
const { api, account, contract, fee } = this;
const request = contract.functions.find((fn) => fn.name === 'request');
const options = { from: account, value: fee.toString() };
const values = this.requestValues();
let chain = Promise.resolve();
this.shallSkipRequest(values)
.then((skipRequest) => {
if (skipRequest) {
return;
}
if (!hasRequested) {
this.step = POSTING_REQUEST;
chain = request.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return request.postTransaction(options, values);
})
.then((handle) => {
// TODO: The "request rejected" error doesn't have any property to
// distinguish it from other errors, so we can't give a meaningful error here.
return api.pollMethod('parity_checkRequest', handle);
})
.then((txHash) => {
this.requestTx = txHash;
return checkIfTxFailed(api, txHash, options.gas)
.then((hasFailed) => {
if (hasFailed) {
throw new Error('Transaction failed, all gas used up.');
}
this.step = POSTED_REQUEST;
return waitForConfirmations(api, txHash, 1);
});
});
}
chain
this.step = POSTING_REQUEST;
return request.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return request.postTransaction(options, values);
})
.then((handle) => {
// The "request rejected" error doesn't have any property to distinguish
// it from other errors, so we can't give a meaningful error here.
return api.pollMethod('parity_checkRequest', handle);
})
.then((txHash) => {
this.requestTx = txHash;
return checkIfTxFailed(api, txHash, options.gas)
.then((hasFailed) => {
if (hasFailed) {
throw new Error('Transaction failed, all gas used up.');
}
this.step = POSTED_REQUEST;
return waitForConfirmations(api, txHash, 1);
});
});
})
.then(() => this.checkIfReceivedCode())
.then((hasReceived) => {
if (hasReceived) {

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
@ -206,7 +207,7 @@ class Verification extends Component {
const {
step,
isServerRunning, fee, isVerified, hasRequested,
isServerRunning, isAbleToRequest, fee, accountIsVerified, accountHasRequested,
requestTx, isCodeValid, confirmationTx,
setCode
} = this.store;
@ -223,17 +224,37 @@ class Verification extends Component {
if (method === 'sms') {
fields.push({
key: 'number',
label: 'phone number in international format',
hint: 'the SMS will be sent to this number',
label: (
<FormattedMessage
id='ui.verification.gatherData.phoneNumber.label'
defaultMessage='phone number in international format'
/>
),
hint: (
<FormattedMessage
id='ui.verification.gatherData.phoneNumber.hint'
defaultMessage='the SMS will be sent to this number'
/>
),
error: this.store.isNumberValid ? null : 'invalid number',
onChange: this.store.setNumber
});
} else if (method === 'email') {
fields.push({
key: 'email',
label: 'email address',
hint: 'the code will be sent to this address',
error: this.store.isEmailValid ? null : 'invalid email',
label: (
<FormattedMessage
id='ui.verification.gatherData.email.label'
defaultMessage='e-mail address'
/>
),
hint: (
<FormattedMessage
id='ui.verification.gatherData.email.hint'
defaultMessage='the code will be sent to this address'
/>
),
error: this.store.isEmailValid ? null : 'invalid e-mail',
onChange: this.store.setEmail
});
}
@ -241,10 +262,12 @@ class Verification extends Component {
return (
<GatherData
fee={ fee }
hasRequested={ hasRequested }
accountHasRequested={ accountHasRequested }
isServerRunning={ isServerRunning }
isVerified={ isVerified }
method={ method } fields={ fields }
isAbleToRequest={ isAbleToRequest }
accountIsVerified={ accountIsVerified }
method={ method }
fields={ fields }
setConsentGiven={ setConsentGiven }
/>
);

View File

@ -16,10 +16,10 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import AddDapps from './AddDapps';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DappPermissions from './DappPermissions';
import DappsVisible from './AddDapps';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@ -37,10 +37,10 @@ import WalletSettings from './WalletSettings';
export {
AddAddress,
AddContract,
AddDapps,
CreateAccount,
CreateWallet,
DappPermissions,
DappsVisible,
DeleteAccount,
DeployContract,
EditMeta,

View File

@ -29,15 +29,14 @@ export default class Container extends Component {
className: PropTypes.string,
compact: PropTypes.bool,
light: PropTypes.bool,
onClick: PropTypes.func,
style: PropTypes.object,
tabIndex: PropTypes.number,
title: nodeOrStringProptype()
}
render () {
const { children, className, compact, light, style, tabIndex } = this.props;
const classes = `${styles.container} ${light ? styles.light : ''} ${className}`;
const { children, className, compact, light, onClick, style, tabIndex } = this.props;
const props = {};
if (Number.isInteger(tabIndex)) {
@ -45,8 +44,27 @@ export default class Container extends Component {
}
return (
<div className={ classes } style={ style } { ...props }>
<Card className={ compact ? styles.compact : styles.padded }>
<div
className={
[
styles.container,
light
? styles.light
: '',
className
].join(' ')
}
style={ style }
{ ...props }
>
<Card
className={
compact
? styles.compact
: styles.padded
}
onClick={ onClick }
>
{ this.renderTitle() }
{ children }
</Card>

View File

@ -17,59 +17,80 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Container, ContainerTitle, DappIcon, Tags } from '~/ui';
import Container, { Title as ContainerTitle } from '~/ui/Container';
import DappIcon from '~/ui/DappIcon';
import Tags from '~/ui/Tags';
import styles from './summary.css';
import styles from './dappCard.css';
export default class Summary extends Component {
export default class DappCard extends Component {
static propTypes = {
app: PropTypes.object.isRequired,
children: PropTypes.node
}
children: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func,
showLink: PropTypes.bool,
showTags: PropTypes.bool
};
static defaultProps = {
showLink: false,
showTags: false
};
render () {
const { app } = this.props;
const { app, children, className, onClick, showLink, showTags } = this.props;
if (!app) {
return null;
}
const link = this.renderLink(app);
return (
<Container className={ styles.container }>
<Container
className={
[styles.container, className].join(' ')
}
onClick={ onClick }
>
<DappIcon
app={ app }
className={ styles.image }
/>
<Tags tags={ [app.type] } />
<Tags
tags={
showTags
? [app.type]
: null
}
/>
<div className={ styles.description }>
<ContainerTitle
className={ styles.title }
title={ link }
title={
showLink
? this.renderLink(app)
: app.name
}
byline={ app.description }
/>
<div className={ styles.author }>
{ app.author }, v{ app.version }
</div>
{ this.props.children }
{ children }
</div>
</Container>
);
}
renderLink (app) {
// Special case for web dapp
if (app.url === 'web') {
return (
<Link to={ `/web` }>
{ app.name }
</Link>
);
}
return (
<Link to={ `/app/${app.id}` }>
<Link
to={
app.url === 'web'
? '/web'
: `/app/${app.id}`
}
>
{ app.name }
</Link>
);

View File

@ -14,4 +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 from './summary';
export default from './dappCard';

View File

@ -30,6 +30,7 @@ import Container, { Title as ContainerTitle } from './Container';
import ContextProvider from './ContextProvider';
import CopyToClipboard from './CopyToClipboard';
import CurrencySymbol from './CurrencySymbol';
import DappCard from './DappCard';
import DappIcon from './DappIcon';
import Editor from './Editor';
import Errors from './Errors';
@ -79,6 +80,7 @@ export {
CopyToClipboard,
CurrencySymbol,
DappIcon,
DappCard,
Editor,
Errors,
FEATURES,

View File

@ -21,14 +21,13 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { AddDapps, DappPermissions } from '~/modals';
import { DappPermissions, DappsVisible } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store';
import { Actionbar, Button, Page } from '~/ui';
import { Actionbar, Button, DappCard, Page } from '~/ui';
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
import UrlButton from './UrlButton';
import DappsStore from './dappsStore';
import Summary from './Summary';
import styles from './dapps.css';
@ -82,8 +81,8 @@ class Dapps extends Component {
return (
<div>
<AddDapps store={ this.store } />
<DappPermissions store={ this.permissionStore } />
<DappsVisible store={ this.store } />
<Actionbar
className={ styles.toolbar }
title={
@ -146,7 +145,11 @@ class Dapps extends Component {
className={ styles.item }
key={ app.id }
>
<Summary app={ app } />
<DappCard
app={ app }
showLink
showTags
/>
</div>
);
}

View File

@ -192,7 +192,7 @@ pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider
{
let network_id = client.signing_network_id();
let min_block = filled.min_block.clone();
let condition = filled.condition.clone();
let signed_transaction = sign_no_dispatch(client, miner, accounts, filled, password)?;
let (signed_transaction, token) = match signed_transaction {
@ -201,7 +201,7 @@ pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider
};
trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id);
let pending_transaction = PendingTransaction::new(signed_transaction, min_block);
let pending_transaction = PendingTransaction::new(signed_transaction, condition.map(Into::into));
dispatch_transaction(&*client, &*miner, pending_transaction).map(|hash| {
match token {
Some(ref token) => WithToken::Yes(hash, token.clone()),
@ -222,7 +222,7 @@ pub fn fill_optional_fields<C, M>(request: TransactionRequest, default_sender: A
gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()),
value: request.value.unwrap_or_else(|| 0.into()),
data: request.data.unwrap_or_else(Vec::new),
min_block: request.min_block,
condition: request.condition,
}
}

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use util::{Address, U256, Bytes};
use v1::types::TransactionCondition;
/// Transaction request coming from RPC
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
@ -33,8 +34,8 @@ pub struct TransactionRequest {
pub data: Option<Bytes>,
/// Transaction's nonce
pub nonce: Option<U256>,
/// Delay until this block if specified.
pub min_block: Option<u64>,
/// Delay until this condition is met.
pub condition: Option<TransactionCondition>,
}
/// Transaction request coming from RPC with default values filled in.
@ -56,8 +57,8 @@ pub struct FilledTransactionRequest {
pub data: Bytes,
/// Transaction's nonce
pub nonce: Option<U256>,
/// Delay until this block if specified.
pub min_block: Option<u64>,
/// Delay until this condition is met.
pub condition: Option<TransactionCondition>,
}
impl From<FilledTransactionRequest> for TransactionRequest {
@ -70,7 +71,7 @@ impl From<FilledTransactionRequest> for TransactionRequest {
value: Some(r.value),
data: Some(r.data),
nonce: r.nonce,
min_block: r.min_block,
condition: r.condition,
}
}
}

View File

@ -349,7 +349,7 @@ mod test {
value: 10_000_000.into(),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})
}

View File

@ -130,7 +130,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
.into_iter().collect::<HashSet<_>>();
let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
let other = store.addresses_info().expect("addresses_info always returns Ok; qed");
let other = store.addresses_info();
Ok(info
.into_iter()

View File

@ -51,22 +51,27 @@ impl<C> ParityAccountsClient<C> where C: MiningBlockChainClient {
}
impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlockChainClient {
fn all_accounts_info(&self) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> {
fn all_accounts_info(&self) -> Result<BTreeMap<RpcH160, BTreeMap<String, String>>, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
let other = store.addresses_info().expect("addresses_info always returns Ok; qed");
let other = store.addresses_info();
Ok(info.into_iter().chain(other.into_iter()).map(|(a, v)| {
let mut m = map![
"name".to_owned() => v.name,
"meta".to_owned() => v.meta
];
if let &Some(ref uuid) = &v.uuid {
m.insert("uuid".to_owned(), format!("{}", uuid));
}
(format!("0x{}", a.hex()), m)
}).collect())
Ok(info
.into_iter()
.chain(other.into_iter())
.map(|(address, v)| {
let mut m = map![
"name".to_owned() => v.name,
"meta".to_owned() => v.meta
];
if let &Some(ref uuid) = &v.uuid {
m.insert("uuid".to_owned(), format!("{}", uuid));
}
(address.into(), m)
})
.collect()
)
}
fn new_account_from_phrase(&self, phrase: String, pass: String) -> Result<RpcH160, Error> {
@ -132,8 +137,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
let store = take_weak!(self.accounts);
let addr: Address = addr.into();
store.remove_address(addr)
.expect("remove_address always returns Ok; qed");
store.remove_address(addr);
Ok(true)
}
@ -143,8 +147,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
let addr: Address = addr.into();
store.set_account_name(addr.clone(), name.clone())
.or_else(|_| store.set_address_name(addr, name))
.expect("set_address_name always returns Ok; qed");
.unwrap_or_else(|_| store.set_address_name(addr, name));
Ok(true)
}
@ -154,8 +157,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
let addr: Address = addr.into();
store.set_account_meta(addr.clone(), meta.clone())
.or_else(|_| store.set_address_meta(addr, meta))
.expect("set_address_meta always returns Ok; qed");
.unwrap_or_else(|_| store.set_address_meta(addr, meta));
Ok(true)
}

View File

@ -87,8 +87,8 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient,
if let Some(gas) = modification.gas {
request.gas = gas.into();
}
if let Some(ref min_block) = modification.min_block {
request.min_block = min_block.as_ref().and_then(|b| b.to_min_block_num());
if let Some(ref condition) = modification.condition {
request.condition = condition.clone().map(Into::into);
}
}
let result = f(&*client, &*miner, &*accounts, payload);
@ -160,7 +160,7 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC
// Dispatch if everything is ok
if sender_matches && data_matches && value_matches && nonce_matches {
let pending_transaction = PendingTransaction::new(signed_transaction, request.min_block);
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
dispatch_transaction(&*client, &*miner, pending_transaction)
.map(Into::into)
.map(ConfirmationResponse::SendTransaction)

View File

@ -212,7 +212,7 @@ impl MinerService for TestMinerService {
self.local_transactions.lock().iter().map(|(hash, stats)| (*hash, stats.clone())).collect()
}
fn ready_transactions(&self, _best_block: BlockNumber) -> Vec<PendingTransaction> {
fn ready_transactions(&self, _best_block: BlockNumber, _best_timestamp: u64) -> Vec<PendingTransaction> {
self.pending_transactions.lock().values().cloned().map(Into::into).collect()
}

View File

@ -365,6 +365,8 @@ fn rpc_eth_accounts() {
let tester = EthTester::default();
let address = tester.accounts_provider.new_account("").unwrap();
tester.accounts_provider.set_new_dapps_whitelist(None).unwrap();
tester.accounts_provider.set_address_name(1.into(), "1".into());
tester.accounts_provider.set_address_name(10.into(), "10".into());
// with current policy it should return the account
let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#;
@ -514,7 +516,7 @@ fn rpc_eth_pending_transaction_by_hash() {
tester.miner.pending_transactions.lock().insert(H256::zero(), tx);
}
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","minBlock":null,"networkId":null,"nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","standardV":"0x0","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":"0x1b","value":"0xa"},"id":1}"#;
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"condition":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","networkId":null,"nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","standardV":"0x0","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":"0x1b","value":"0xa"},"id":1}"#;
let request = r#"{
"jsonrpc": "2.0",
"method": "eth_getTransactionByHash",
@ -830,12 +832,11 @@ fn rpc_eth_sign_transaction() {
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
r#""tx":{"# +
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
&format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","# +
r#""minBlock":null,"# +
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
r#""nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.recover_public().unwrap()) +

View File

@ -120,10 +120,11 @@ fn should_be_able_to_set_meta() {
fn rpc_parity_set_and_get_dapps_accounts() {
// given
let tester = setup();
tester.accounts.set_address_name(10.into(), "10".into());
assert_eq!(tester.accounts.dapps_addresses("app1".into()).unwrap(), vec![]);
// when
let request = r#"{"jsonrpc": "2.0", "method": "parity_setDappsAddresses","params":["app1",["0x000000000000000000000000000000000000000a"]], "id": 1}"#;
let request = r#"{"jsonrpc": "2.0", "method": "parity_setDappsAddresses","params":["app1",["0x000000000000000000000000000000000000000a","0x0000000000000000000000000000000000000001"]], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));

View File

@ -85,7 +85,7 @@ fn should_return_list_of_items_to_confirm() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
tester.signer.add_request(ConfirmationPayload::Signature(1.into(), vec![5].into())).unwrap();
@ -93,7 +93,7 @@ fn should_return_list_of_items_to_confirm() {
let request = r#"{"jsonrpc":"2.0","method":"signer_requestsToConfirm","params":[],"id":1}"#;
let response = concat!(
r#"{"jsonrpc":"2.0","result":["#,
r#"{"id":"0x1","payload":{"sendTransaction":{"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","minBlock":null,"nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
r#"{"id":"0x1","payload":{"sendTransaction":{"condition":null,"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
r#"{"id":"0x2","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}}}"#,
r#"],"id":1}"#
);
@ -116,7 +116,7 @@ fn should_reject_transaction_from_queue_without_dispatching() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
@ -143,7 +143,7 @@ fn should_not_remove_transaction_if_password_is_invalid() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
@ -187,7 +187,7 @@ fn should_confirm_transaction_and_dispatch() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
let t = Transaction {
@ -233,7 +233,7 @@ fn should_alter_the_sender_and_nonce() {
value: U256::from(1),
data: vec![],
nonce: Some(10.into()),
min_block: None,
condition: None,
})).unwrap();
let t = Transaction {
@ -283,7 +283,7 @@ fn should_confirm_transaction_with_token() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
let t = Transaction {
@ -332,7 +332,7 @@ fn should_confirm_transaction_with_rlp() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
let t = Transaction {
@ -380,7 +380,7 @@ fn should_return_error_when_sender_does_not_match() {
value: U256::from(1),
data: vec![],
nonce: None,
min_block: None,
condition: None,
})).unwrap();
let t = Transaction {

View File

@ -277,12 +277,11 @@ fn should_add_sign_transaction_to_the_queue() {
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
r#""tx":{"# +
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
&format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","# +
r#""minBlock":null,"# +
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
r#""nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key()) +

View File

@ -25,7 +25,7 @@ build_rpc_trait! {
pub trait ParityAccounts {
/// Returns accounts information.
#[rpc(name = "parity_allAccountsInfo")]
fn all_accounts_info(&self) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error>;
fn all_accounts_info(&self) -> Result<BTreeMap<H160, BTreeMap<String, String>>, Error>;
/// Creates new account from the given phrase using standard brainwallet mechanism.
/// Second parameter is password for the new account.

View File

@ -139,7 +139,7 @@ mod tests {
fn test_serialize_block_transactions() {
let t = BlockTransactions::Full(vec![Transaction::default()]);
let serialized = serde_json::to_string(&t).unwrap();
assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","minBlock":null}]"#);
assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","condition":null}]"#);
let t = BlockTransactions::Hashes(vec![H256::default().into()]);
let serialized = serde_json::to_string(&t).unwrap();

View File

@ -21,7 +21,7 @@ use serde::{Serialize, Serializer};
use util::log::Colour;
use util::bytes::ToPretty;
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, BlockNumber};
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, TransactionCondition};
use v1::helpers;
/// Confirmation waiting in a queue
@ -196,9 +196,8 @@ pub struct TransactionModification {
pub gas_price: Option<U256>,
/// Modified gas
pub gas: Option<U256>,
/// Modified min block
#[serde(rename="minBlock")]
pub min_block: Option<Option<BlockNumber>>,
/// Modified transaction condition.
pub condition: Option<Option<TransactionCondition>>,
}
/// Represents two possible return values.
@ -240,7 +239,7 @@ impl<A, B> Serialize for Either<A, B> where
mod tests {
use std::str::FromStr;
use serde_json;
use v1::types::{U256, H256, BlockNumber};
use v1::types::{U256, H256, TransactionCondition};
use v1::helpers;
use super::*;
@ -274,13 +273,13 @@ mod tests {
value: 100_000.into(),
data: vec![1, 2, 3],
nonce: Some(1.into()),
min_block: None,
condition: None,
}),
};
// when
let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","minBlock":null}}}"#;
let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}}}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
@ -300,13 +299,13 @@ mod tests {
value: 100_000.into(),
data: vec![1, 2, 3],
nonce: Some(1.into()),
min_block: None,
condition: None,
}),
};
// when
let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","minBlock":null}}}"#;
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}}}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
@ -336,7 +335,7 @@ mod tests {
let s1 = r#"{
"sender": "0x000000000000000000000000000000000000000a",
"gasPrice":"0xba43b7400",
"minBlock":"0x42"
"condition": { "block": 66 }
}"#;
let s2 = r#"{"gas": "0x1233"}"#;
let s3 = r#"{}"#;
@ -351,19 +350,19 @@ mod tests {
sender: Some(10.into()),
gas_price: Some(U256::from_str("0ba43b7400").unwrap()),
gas: None,
min_block: Some(Some(BlockNumber::Num(0x42))),
condition: Some(Some(TransactionCondition::Number(0x42))),
});
assert_eq!(res2, TransactionModification {
sender: None,
gas_price: None,
gas: Some(U256::from_str("1233").unwrap()),
min_block: None,
condition: None,
});
assert_eq!(res3, TransactionModification {
sender: None,
gas_price: None,
gas: None,
min_block: None,
condition: None,
});
}

View File

@ -27,6 +27,7 @@ mod log;
mod sync;
mod transaction;
mod transaction_request;
mod transaction_condition;
mod receipt;
mod rpc_settings;
mod trace;
@ -55,6 +56,7 @@ pub use self::sync::{
};
pub use self::transaction::{Transaction, RichRawTransaction, LocalTransactionStatus};
pub use self::transaction_request::TransactionRequest;
pub use self::transaction_condition::TransactionCondition;
pub use self::receipt::Receipt;
pub use self::rpc_settings::RpcSettings;
pub use self::trace::{LocalizedTrace, TraceResults};

View File

@ -19,7 +19,7 @@ use ethcore::miner;
use ethcore::contract_address;
use ethcore::transaction::{LocalizedTransaction, Action, PendingTransaction, SignedTransaction};
use v1::helpers::errors;
use v1::types::{Bytes, H160, H256, U256, H512, BlockNumber};
use v1::types::{Bytes, H160, H256, U256, H512, TransactionCondition};
/// Transaction
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
@ -70,8 +70,7 @@ pub struct Transaction {
/// The S field of the signature.
pub s: U256,
/// Transaction activates at specified block.
#[serde(rename="minBlock")]
pub min_block: Option<BlockNumber>,
pub condition: Option<TransactionCondition>,
}
/// Local Transaction Status
@ -190,7 +189,7 @@ impl From<LocalizedTransaction> for Transaction {
v: t.original_v().into(),
r: signature.r().into(),
s: signature.s().into(),
min_block: None,
condition: None,
}
}
}
@ -224,7 +223,7 @@ impl From<SignedTransaction> for Transaction {
v: t.original_v().into(),
r: signature.r().into(),
s: signature.s().into(),
min_block: None,
condition: None,
}
}
}
@ -232,7 +231,7 @@ impl From<SignedTransaction> for Transaction {
impl From<PendingTransaction> for Transaction {
fn from(t: PendingTransaction) -> Transaction {
let mut r = Transaction::from(t.transaction);
r.min_block = t.min_block.map(|b| BlockNumber::Num(b));
r.condition = t.condition.map(|b| b.into());
r
}
}
@ -261,7 +260,7 @@ mod tests {
fn test_transaction_serialize() {
let t = Transaction::default();
let serialized = serde_json::to_string(&t).unwrap();
assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","minBlock":null}"#);
assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","condition":null}"#);
}
#[test]

View File

@ -0,0 +1,67 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use ethcore;
/// Represents condition on minimum block number or block timestamp.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum TransactionCondition {
/// Valid at this minimum block number.
#[serde(rename="block")]
Number(u64),
/// Valid at given unix time.
#[serde(rename="time")]
Timestamp(u64),
}
impl Into<ethcore::transaction::Condition> for TransactionCondition {
fn into(self) -> ethcore::transaction::Condition {
match self {
TransactionCondition::Number(n) => ethcore::transaction::Condition::Number(n),
TransactionCondition::Timestamp(n) => ethcore::transaction::Condition::Timestamp(n),
}
}
}
impl From<ethcore::transaction::Condition> for TransactionCondition {
fn from(condition: ethcore::transaction::Condition) -> Self {
match condition {
ethcore::transaction::Condition::Number(n) => TransactionCondition::Number(n),
ethcore::transaction::Condition::Timestamp(n) => TransactionCondition::Timestamp(n),
}
}
}
#[cfg(test)]
mod tests {
use ethcore;
use super::*;
use serde_json;
#[test]
fn condition_deserialization() {
let s = r#"[{ "block": 51 }, { "time": 10 }]"#;
let deserialized: Vec<TransactionCondition> = serde_json::from_str(s).unwrap();
assert_eq!(deserialized, vec![TransactionCondition::Number(51), TransactionCondition::Timestamp(10)])
}
#[test]
fn condition_into() {
assert_eq!(ethcore::transaction::Condition::Number(100), TransactionCondition::Number(100).into());
assert_eq!(ethcore::transaction::Condition::Timestamp(100), TransactionCondition::Timestamp(100).into());
}
}

View File

@ -16,7 +16,7 @@
//! `TransactionRequest` type
use v1::types::{Bytes, H160, U256, BlockNumber};
use v1::types::{Bytes, H160, U256, TransactionCondition};
use v1::helpers;
use util::log::Colour;
@ -41,9 +41,8 @@ pub struct TransactionRequest {
pub data: Option<Bytes>,
/// Transaction's nonce
pub nonce: Option<U256>,
/// Delay until this block if specified.
#[serde(rename="minBlock")]
pub min_block: Option<BlockNumber>,
/// Delay until this block condition.
pub condition: Option<TransactionCondition>,
}
pub fn format_ether(i: U256) -> String {
@ -93,7 +92,7 @@ impl From<helpers::TransactionRequest> for TransactionRequest {
value: r.value.map(Into::into),
data: r.data.map(Into::into),
nonce: r.nonce.map(Into::into),
min_block: r.min_block.map(|b| BlockNumber::Num(b)),
condition: r.condition.map(Into::into),
}
}
}
@ -108,7 +107,7 @@ impl From<helpers::FilledTransactionRequest> for TransactionRequest {
value: Some(r.value.into()),
data: Some(r.data.into()),
nonce: r.nonce.map(Into::into),
min_block: r.min_block.map(|b| BlockNumber::Num(b)),
condition: r.condition.map(Into::into),
}
}
}
@ -123,7 +122,7 @@ impl Into<helpers::TransactionRequest> for TransactionRequest {
value: self.value.map(Into::into),
data: self.data.map(Into::into),
nonce: self.nonce.map(Into::into),
min_block: self.min_block.and_then(|b| b.to_min_block_num()),
condition: self.condition.map(Into::into),
}
}
}
@ -134,7 +133,7 @@ mod tests {
use std::str::FromStr;
use rustc_serialize::hex::FromHex;
use serde_json;
use v1::types::{U256, H160, BlockNumber};
use v1::types::{U256, H160, TransactionCondition};
use super::*;
#[test]
@ -147,7 +146,7 @@ mod tests {
"value":"0x3",
"data":"0x123456",
"nonce":"0x4",
"minBlock":"0x13"
"condition": { "block": 19 }
}"#;
let deserialized: TransactionRequest = serde_json::from_str(s).unwrap();
@ -159,7 +158,7 @@ mod tests {
value: Some(U256::from(3)),
data: Some(vec![0x12, 0x34, 0x56].into()),
nonce: Some(U256::from(4)),
min_block: Some(BlockNumber::Num(0x13)),
condition: Some(TransactionCondition::Number(0x13)),
});
}
@ -183,7 +182,7 @@ mod tests {
value: Some(U256::from_str("9184e72a").unwrap()),
data: Some("d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675".from_hex().unwrap().into()),
nonce: None,
min_block: None,
condition: None,
});
}
@ -200,7 +199,7 @@ mod tests {
value: None,
data: None,
nonce: None,
min_block: None,
condition: None,
});
}
@ -224,7 +223,7 @@ mod tests {
value: None,
data: Some(vec![0x85, 0x95, 0xba, 0xb1].into()),
nonce: None,
min_block: None,
condition: None,
});
}

View File

@ -1,5 +1,5 @@
use client::{Rpc, RpcError};
use rpc::v1::types::{ConfirmationRequest, TransactionModification, U256, BlockNumber};
use rpc::v1::types::{ConfirmationRequest, TransactionModification, U256, TransactionCondition};
use serde_json::{Value as JsonValue, to_value};
use std::path::PathBuf;
use futures::{BoxFuture, Canceled};
@ -22,13 +22,13 @@ impl SignerRpc {
id: U256,
new_gas: Option<U256>,
new_gas_price: Option<U256>,
new_min_block: Option<Option<BlockNumber>>,
new_condition: Option<Option<TransactionCondition>>,
pwd: &str
) -> BoxFuture<Result<U256, RpcError>, Canceled>
{
self.rpc.request("signer_confirmRequest", vec![
to_value(&format!("{:#x}", id)),
to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, min_block: new_min_block }),
to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, condition: new_condition }),
to_value(&pwd),
])
}

View File

@ -25,7 +25,7 @@ echo "Homepage: https://ethcore.io" >> $control
echo "Vcs-Git: git://github.com/ethcore/parity.git" >> $control
echo "Vcs-Browser: https://github.com/ethcore/parity" >> $control
echo "Architecture: $1" >> $control
echo "Depends: libssl1.0.0" >> $control
echo "Depends: libssl1.0.0 (>=1.0.0)" >> $control
echo "Description: Ethereum network client by Ethcore" >> $control
#build .deb package

View File

@ -21,7 +21,7 @@ rust-crypto = "0.2.34"
elastic-array = { git = "https://github.com/ethcore/elastic-array" }
rlp = { path = "rlp" }
heapsize = { version = "0.3", features = ["unstable"] }
itertools = "0.4"
itertools = "0.5"
sha3 = { path = "sha3" }
clippy = { version = "0.0.103", optional = true}
ethcore-devtools = { path = "../devtools" }

View File

@ -9,8 +9,9 @@ authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
futures = "0.1"
futures-cpupool = "0.1"
parking_lot = "0.3"
log = "0.3"
reqwest = "0.2"
reqwest = "0.4"
mime = "0.2"
clippy = { version = "0.0.90", optional = true}

View File

@ -16,13 +16,14 @@
//! Fetching
use std::{io, fmt};
use std::{io, fmt, time};
use std::sync::Arc;
use std::sync::atomic::{self, AtomicBool};
use futures::{self, BoxFuture, Future};
use futures_cpupool::{CpuPool, CpuFuture};
use mime::{self, Mime};
use parking_lot::RwLock;
use reqwest;
#[derive(Default, Debug, Clone)]
@ -73,24 +74,52 @@ pub trait Fetch: Clone + Send + Sync + 'static {
fn close(self) where Self: Sized {}
}
#[derive(Clone)]
const CLIENT_TIMEOUT_SECONDS: u64 = 5;
pub struct Client {
client: Arc<reqwest::Client>,
client: RwLock<(time::Instant, Arc<reqwest::Client>)>,
pool: CpuPool,
limit: Option<usize>,
}
impl Clone for Client {
fn clone(&self) -> Self {
let (ref time, ref client) = *self.client.read();
Client {
client: RwLock::new((time.clone(), client.clone())),
pool: self.pool.clone(),
limit: self.limit.clone(),
}
}
}
impl Client {
fn with_limit(limit: Option<usize>) -> Result<Self, Error> {
fn new_client() -> Result<Arc<reqwest::Client>, Error> {
let mut client = reqwest::Client::new()?;
client.redirect(reqwest::RedirectPolicy::limited(5));
Ok(Arc::new(client))
}
fn with_limit(limit: Option<usize>) -> Result<Self, Error> {
Ok(Client {
client: Arc::new(client),
client: RwLock::new((time::Instant::now(), Self::new_client()?)),
pool: CpuPool::new(4),
limit: limit,
})
}
fn client(&self) -> Result<Arc<reqwest::Client>, Error> {
{
let (ref time, ref client) = *self.client.read();
if time.elapsed() < time::Duration::from_secs(CLIENT_TIMEOUT_SECONDS) {
return Ok(client.clone());
}
}
let client = Self::new_client()?;
*self.client.write() = (time::Instant::now(), client.clone());
Ok(client)
}
}
impl Fetch for Client {
@ -112,12 +141,19 @@ impl Fetch for Client {
fn fetch_with_abort(&self, url: &str, abort: Abort) -> Self::Result {
debug!(target: "fetch", "Fetching from: {:?}", url);
self.pool.spawn(FetchTask {
url: url.into(),
client: self.client.clone(),
limit: self.limit,
abort: abort,
})
match self.client() {
Ok(client) => {
self.pool.spawn(FetchTask {
url: url.into(),
client: client,
limit: self.limit,
abort: abort,
})
},
Err(err) => {
self.pool.spawn(futures::future::err(err))
},
}
}
}

View File

@ -21,6 +21,7 @@ extern crate log;
extern crate futures;
extern crate futures_cpupool;
extern crate parking_lot;
extern crate reqwest;
pub extern crate mime;

View File

@ -77,7 +77,9 @@ pub fn ordered_trie_root<I>(input: I) -> H256
/// assert_eq!(trie_root(v), H256::from_str(root).unwrap());
/// }
/// ```
pub fn trie_root(input: Vec<(Vec<u8>, Vec<u8>)>) -> H256 {
pub fn trie_root<I>(input: I) -> H256
where I: IntoIterator<Item=(Vec<u8>, Vec<u8>)>
{
let gen_input = input
// first put elements into btree to sort them and to remove duplicates
.into_iter()