Merge branch 'master' into client-provider

This commit is contained in:
Robert Habermeier 2016-11-21 12:19:11 +01:00
commit 06f5bf809f
109 changed files with 3869 additions and 555 deletions

18
Cargo.lock generated
View File

@ -11,6 +11,7 @@ dependencies = [
"ethcore 1.5.0", "ethcore 1.5.0",
"ethcore-dapps 1.5.0", "ethcore-dapps 1.5.0",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.4.0",
"ethcore-hash-fetch 1.5.0",
"ethcore-io 1.5.0", "ethcore-io 1.5.0",
"ethcore-ipc 1.4.0", "ethcore-ipc 1.4.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.4.0",
@ -298,6 +299,7 @@ dependencies = [
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lru-cache 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
@ -334,8 +336,8 @@ version = "1.5.0"
dependencies = [ dependencies = [
"clippy 0.0.96 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.96 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.4.0",
"ethcore-hash-fetch 1.5.0",
"ethcore-rpc 1.5.0", "ethcore-rpc 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"fetch 0.1.0", "fetch 0.1.0",
@ -366,6 +368,18 @@ dependencies = [
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "ethcore-hash-fetch"
version = "1.5.0"
dependencies = [
"ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-util 1.5.0",
"fetch 0.1.0",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "ethcore-io" name = "ethcore-io"
version = "1.5.0" version = "1.5.0"
@ -1250,7 +1264,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#b2513e92603b473799d653583bd86771e0063c08" source = "git+https://github.com/ethcore/js-precompiled.git#587684374a12bf715151dd987a552a3d61e42972"
dependencies = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -43,6 +43,7 @@ ethcore-ipc-nano = { path = "ipc/nano" }
ethcore-ipc = { path = "ipc/rpc" } ethcore-ipc = { path = "ipc/rpc" }
ethcore-ipc-hypervisor = { path = "ipc/hypervisor" } ethcore-ipc-hypervisor = { path = "ipc/hypervisor" }
ethcore-logger = { path = "logger" } ethcore-logger = { path = "logger" }
ethcore-hash-fetch = { path = "ethcore/hash-fetch" }
rlp = { path = "util/rlp" } rlp = { path = "util/rlp" }
ethcore-stratum = { path = "stratum" } ethcore-stratum = { path = "stratum" }
ethcore-dapps = { path = "dapps", optional = true } ethcore-dapps = { path = "dapps", optional = true }

View File

@ -103,6 +103,14 @@ $ cargo build --release
This will produce an executable in the `./target/release` subdirectory. This will produce an executable in the `./target/release` subdirectory.
----
## Simple one-line installer for Mac and Ubuntu
```bash
bash <(curl https://get.parity.io -Lk)
```
## Start Parity ## Start Parity
### Manually ### Manually
To start Parity manually, just run To start Parity manually, just run

View File

@ -20,20 +20,20 @@ url = "1.0"
rustc-serialize = "0.3" rustc-serialize = "0.3"
serde = "0.8" serde = "0.8"
serde_json = "0.8" serde_json = "0.8"
ethabi = "0.2.2"
linked-hash-map = "0.3" linked-hash-map = "0.3"
parity-dapps-glue = "1.4" parity-dapps-glue = "1.4"
mime = "0.2" mime = "0.2"
mime_guess = "1.6.1"
time = "0.1.35" time = "0.1.35"
serde_macros = { version = "0.8", optional = true } serde_macros = { version = "0.8", optional = true }
zip = { version = "0.1", default-features = false } zip = { version = "0.1", default-features = false }
ethcore-devtools = { path = "../devtools" } ethcore-devtools = { path = "../devtools" }
ethcore-rpc = { path = "../rpc" } ethcore-rpc = { path = "../rpc" }
ethcore-util = { path = "../util" } ethcore-util = { path = "../util" }
ethcore-hash-fetch = { path = "../ethcore/hash-fetch" }
fetch = { path = "../util/fetch" } fetch = { path = "../util/fetch" }
parity-ui = { path = "./ui" } parity-ui = { path = "./ui" }
mime_guess = { version = "1.6.1" }
clippy = { version = "0.0.96", optional = true} clippy = { version = "0.0.96", optional = true}
[build-dependencies] [build-dependencies]

View File

@ -24,6 +24,7 @@ use std::io::{self, Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use rustc_serialize::hex::FromHex; use rustc_serialize::hex::FromHex;
use hash_fetch::urlhint::{URLHintContract, URLHint, URLHintResult};
use hyper; use hyper;
use hyper::status::StatusCode; use hyper::status::StatusCode;
@ -37,7 +38,6 @@ use handlers::{ContentHandler, ContentFetcherHandler, ContentValidator};
use endpoint::{Endpoint, EndpointPath, Handler}; use endpoint::{Endpoint, EndpointPath, Handler};
use apps::cache::{ContentCache, ContentStatus}; use apps::cache::{ContentCache, ContentStatus};
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest, serialize_manifest, Manifest}; use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest, serialize_manifest, Manifest};
use apps::urlhint::{URLHintContract, URLHint, URLHintResult};
/// Limit of cached dapps/content /// Limit of cached dapps/content
const MAX_CACHED_DAPPS: usize = 20; const MAX_CACHED_DAPPS: usize = 20;
@ -402,10 +402,11 @@ mod tests {
use std::env; use std::env;
use std::sync::Arc; use std::sync::Arc;
use util::Bytes; use util::Bytes;
use hash_fetch::urlhint::{URLHint, URLHintResult};
use apps::cache::ContentStatus;
use endpoint::EndpointInfo; use endpoint::EndpointInfo;
use page::LocalPageEndpoint; use page::LocalPageEndpoint;
use apps::cache::ContentStatus;
use apps::urlhint::{URLHint, URLHintResult};
use super::ContentFetcher; use super::ContentFetcher;
struct FakeResolver; struct FakeResolver;

View File

@ -21,7 +21,6 @@ use parity_dapps::WebApp;
mod cache; mod cache;
mod fs; mod fs;
pub mod urlhint;
pub mod fetcher; pub mod fetcher;
pub mod manifest; pub mod manifest;

View File

@ -51,13 +51,13 @@ extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate zip; extern crate zip;
extern crate rand; extern crate rand;
extern crate ethabi;
extern crate jsonrpc_core; extern crate jsonrpc_core;
extern crate jsonrpc_http_server; extern crate jsonrpc_http_server;
extern crate mime_guess; extern crate mime_guess;
extern crate rustc_serialize; extern crate rustc_serialize;
extern crate ethcore_rpc; extern crate ethcore_rpc;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate ethcore_hash_fetch as hash_fetch;
extern crate linked_hash_map; extern crate linked_hash_map;
extern crate fetch; extern crate fetch;
extern crate parity_dapps_glue as parity_dapps; extern crate parity_dapps_glue as parity_dapps;
@ -84,12 +84,11 @@ mod url;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use self::apps::urlhint::ContractClient;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::collections::HashMap; use std::collections::HashMap;
use hash_fetch::urlhint::ContractClient;
use jsonrpc_core::{IoHandler, IoDelegate}; use jsonrpc_core::{IoHandler, IoDelegate};
use router::auth::{Authorization, NoAuth, HttpBasicAuth}; use router::auth::{Authorization, NoAuth, HttpBasicAuth};
use ethcore_rpc::Extendable; use ethcore_rpc::Extendable;
@ -219,7 +218,7 @@ impl Server {
) -> Result<Server, ServerError> { ) -> Result<Server, ServerError> {
let panic_handler = Arc::new(Mutex::new(None)); let panic_handler = Arc::new(Mutex::new(None));
let authorization = Arc::new(authorization); let authorization = Arc::new(authorization);
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status, signer_address.clone())); let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(hash_fetch::urlhint::URLHintContract::new(registrar), sync_status, signer_address.clone()));
let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_address.clone())); let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_address.clone()));
let cors_domains = Self::cors_domains(signer_address.clone()); let cors_domains = Self::cors_domains(signer_address.clone());

View File

@ -22,7 +22,7 @@ use env_logger::LogBuilder;
use ServerBuilder; use ServerBuilder;
use Server; use Server;
use apps::urlhint::ContractClient; use hash_fetch::urlhint::ContractClient;
use util::{Bytes, Address, Mutex, ToPretty}; use util::{Bytes, Address, Mutex, ToPretty};
use devtools::http_client; use devtools::http_client;

View File

@ -27,6 +27,7 @@ time = "0.1"
rand = "0.3" rand = "0.3"
byteorder = "0.5" byteorder = "0.5"
transient-hashmap = "0.1" transient-hashmap = "0.1"
linked-hash-map = "0.3.0"
evmjit = { path = "../evmjit", optional = true } evmjit = { path = "../evmjit", optional = true }
clippy = { version = "0.0.96", optional = true} clippy = { version = "0.0.96", optional = true}
ethash = { path = "../ethash" } ethash = { path = "../ethash" }

View File

@ -0,0 +1,15 @@
[package]
description = "Fetching hash-addressed content."
homepage = "https://ethcore.io"
license = "GPL-3.0"
name = "ethcore-hash-fetch"
version = "1.5.0"
authors = ["Ethcore <admin@ethcore.io>"]
[dependencies]
log = "0.3"
rustc-serialize = "0.3"
ethabi = "0.2.2"
mime_guess = "1.6.1"
fetch = { path = "../../util/fetch" }
ethcore-util = { path = "../../util" }

View File

@ -0,0 +1,114 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Hash-addressed content resolver & fetcher.
use std::{io, fs};
use std::sync::Arc;
use std::path::PathBuf;
use util::{Mutex, H256, sha3};
use fetch::{Fetch, FetchError, Client as FetchClient};
use urlhint::{ContractClient, URLHintContract, URLHint, URLHintResult};
/// API for fetching by hash.
pub trait HashFetch {
/// Fetch hash-addressed content.
/// Parameters:
/// 1. `hash` - content hash
/// 2. `on_done` - callback function invoked when the content is ready (or there was error during fetch)
///
/// This function may fail immediately when fetch cannot be initialized or content cannot be resolved.
fn fetch(&self, hash: H256, on_done: Box<Fn(Result<PathBuf, Error>) + Send>) -> Result<(), Error>;
}
/// Hash-fetching error.
#[derive(Debug)]
pub enum Error {
/// Hash could not be resolved to a valid content address.
NoResolution,
/// Downloaded content hash does not match.
HashMismatch { expected: H256, got: H256 },
/// IO Error while validating hash.
IO(io::Error),
/// Error during fetch.
Fetch(FetchError),
}
impl From<FetchError> for Error {
fn from(error: FetchError) -> Self {
Error::Fetch(error)
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Error::IO(error)
}
}
/// Default Hash-fetching client using on-chain contract to resolve hashes to URLs.
pub struct Client {
contract: URLHintContract,
fetch: Mutex<FetchClient>,
}
impl Client {
/// Creates new instance of the `Client` given on-chain contract client.
pub fn new(contract: Arc<ContractClient>) -> Self {
Client {
contract: URLHintContract::new(contract),
fetch: Mutex::new(FetchClient::default()),
}
}
}
impl HashFetch for Client {
fn fetch(&self, hash: H256, on_done: Box<Fn(Result<PathBuf, Error>) + Send>) -> Result<(), Error> {
debug!(target: "dapps", "Fetching: {:?}", hash);
let url = try!(
self.contract.resolve(hash.to_vec()).map(|content| match content {
URLHintResult::Dapp(dapp) => {
dapp.url()
},
URLHintResult::Content(content) => {
content.url
},
}).ok_or_else(|| Error::NoResolution)
);
debug!(target: "dapps", "Resolved {:?} to {:?}. Fetching...", hash, url);
self.fetch.lock().request_async(&url, Default::default(), Box::new(move |result| {
fn validate_hash(hash: H256, result: Result<PathBuf, FetchError>) -> Result<PathBuf, Error> {
let path = try!(result);
let mut file_reader = io::BufReader::new(try!(fs::File::open(&path)));
let content_hash = try!(sha3(&mut file_reader));
if content_hash != hash {
Err(Error::HashMismatch{ got: content_hash, expected: hash })
} else {
Ok(path)
}
}
debug!(target: "dapps", "Content fetched, validating hash ({:?})", hash);
on_done(validate_hash(hash, result))
})).map_err(Into::into)
}
}

View File

@ -0,0 +1,33 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Hash-addressed content resolver & fetcher.
#![warn(missing_docs)]
#[macro_use]
extern crate log;
extern crate rustc_serialize;
extern crate mime_guess;
extern crate ethabi;
extern crate ethcore_util as util;
extern crate fetch;
mod client;
pub mod urlhint;
pub use client::{HashFetch, Client};

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! URLHint Contract
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use rustc_serialize::hex::ToHex; use rustc_serialize::hex::ToHex;
@ -24,15 +26,30 @@ use util::{Address, Bytes, Hashable};
const COMMIT_LEN: usize = 20; const COMMIT_LEN: usize = 20;
/// RAW Contract interface.
/// Should execute transaction using current blockchain state.
pub trait ContractClient: Send + Sync {
/// Get registrar address
fn registrar(&self) -> Result<Address, String>;
/// Call Contract
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String>;
}
/// Github-hosted dapp.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct GithubApp { pub struct GithubApp {
/// Github Account
pub account: String, pub account: String,
/// Github Repository
pub repo: String, pub repo: String,
/// Commit on Github
pub commit: [u8;COMMIT_LEN], pub commit: [u8;COMMIT_LEN],
/// Dapp owner address
pub owner: Address, pub owner: Address,
} }
impl GithubApp { impl GithubApp {
/// Returns URL of this Github-hosted dapp package.
pub fn url(&self) -> String { pub fn url(&self) -> String {
// Since https fetcher doesn't support redirections we use direct link // Since https fetcher doesn't support redirections we use direct link
// format!("https://github.com/{}/{}/archive/{}.zip", self.account, self.repo, self.commit.to_hex()) // format!("https://github.com/{}/{}/archive/{}.zip", self.account, self.repo, self.commit.to_hex())
@ -53,22 +70,17 @@ impl GithubApp {
} }
} }
/// Hash-Addressed Content
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Content { pub struct Content {
/// URL of the content
pub url: String, pub url: String,
/// MIME type of the content
pub mime: String, pub mime: String,
/// Content owner address
pub owner: Address, pub owner: Address,
} }
/// RAW Contract interface.
/// Should execute transaction using current blockchain state.
pub trait ContractClient: Send + Sync {
/// Get registrar address
fn registrar(&self) -> Result<Address, String>;
/// Call Contract
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String>;
}
/// Result of resolving id to URL /// Result of resolving id to URL
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum URLHintResult { pub enum URLHintResult {
@ -84,6 +96,7 @@ pub trait URLHint {
fn resolve(&self, id: Bytes) -> Option<URLHintResult>; fn resolve(&self, id: Bytes) -> Option<URLHintResult>;
} }
/// `URLHintContract` API
pub struct URLHintContract { pub struct URLHintContract {
urlhint: Contract, urlhint: Contract,
registrar: Contract, registrar: Contract,
@ -91,9 +104,10 @@ pub struct URLHintContract {
} }
impl URLHintContract { impl URLHintContract {
/// Creates new `URLHintContract`
pub fn new(client: Arc<ContractClient>) -> Self { pub fn new(client: Arc<ContractClient>) -> Self {
let urlhint = Interface::load(include_bytes!("./urlhint.json")).expect("urlhint.json is valid ABI"); let urlhint = Interface::load(include_bytes!("../res/urlhint.json")).expect("urlhint.json is valid ABI");
let registrar = Interface::load(include_bytes!("./registrar.json")).expect("registrar.json is valid ABI"); let registrar = Interface::load(include_bytes!("../res/registrar.json")).expect("registrar.json is valid ABI");
URLHintContract { URLHintContract {
urlhint: Contract::new(urlhint), urlhint: Contract::new(urlhint),
@ -244,11 +258,6 @@ fn guess_mime_type(url: &str) -> Option<String> {
}) })
} }
#[cfg(test)]
pub fn test_guess_mime_type(url: &str) -> Option<String> {
guess_mime_type(url)
}
fn as_string<T: fmt::Debug>(e: T) -> String { fn as_string<T: fmt::Debug>(e: T) -> String {
format!("{:?}", e) format!("{:?}", e)
} }
@ -260,6 +269,7 @@ mod tests {
use rustc_serialize::hex::FromHex; use rustc_serialize::hex::FromHex;
use super::*; use super::*;
use super::guess_mime_type;
use util::{Bytes, Address, Mutex, ToPretty}; use util::{Bytes, Address, Mutex, ToPretty};
struct FakeRegistrar { struct FakeRegistrar {
@ -390,10 +400,10 @@ mod tests {
let url5 = "https://ethcore.io/parity.png"; let url5 = "https://ethcore.io/parity.png";
assert_eq!(test_guess_mime_type(url1), None); assert_eq!(guess_mime_type(url1), None);
assert_eq!(test_guess_mime_type(url2), Some("image/png".into())); assert_eq!(guess_mime_type(url2), Some("image/png".into()));
assert_eq!(test_guess_mime_type(url3), Some("image/png".into())); assert_eq!(guess_mime_type(url3), Some("image/png".into()));
assert_eq!(test_guess_mime_type(url4), Some("image/jpeg".into())); assert_eq!(guess_mime_type(url4), Some("image/jpeg".into()));
assert_eq!(test_guess_mime_type(url5), Some("image/png".into())); assert_eq!(guess_mime_type(url5), Some("image/png".into()));
} }
} }

View File

@ -182,8 +182,8 @@
"enode://89d5dc2a81e574c19d0465f497c1af96732d1b61a41de89c2a37f35707689ac416529fae1038809852b235c2d30fd325abdc57c122feeefbeaaf802cc7e9580d@45.55.33.62:30303", "enode://89d5dc2a81e574c19d0465f497c1af96732d1b61a41de89c2a37f35707689ac416529fae1038809852b235c2d30fd325abdc57c122feeefbeaaf802cc7e9580d@45.55.33.62:30303",
"enode://605e04a43b1156966b3a3b66b980c87b7f18522f7f712035f84576016be909a2798a438b2b17b1a8c58db314d88539a77419ca4be36148c086900fba487c9d39@188.166.255.12:30303", "enode://605e04a43b1156966b3a3b66b980c87b7f18522f7f712035f84576016be909a2798a438b2b17b1a8c58db314d88539a77419ca4be36148c086900fba487c9d39@188.166.255.12:30303",
"enode://016b20125f447a3b203a3cae953b2ede8ffe51290c071e7599294be84317635730c397b8ff74404d6be412d539ee5bb5c3c700618723d3b53958c92bd33eaa82@159.203.210.80:30303", "enode://016b20125f447a3b203a3cae953b2ede8ffe51290c071e7599294be84317635730c397b8ff74404d6be412d539ee5bb5c3c700618723d3b53958c92bd33eaa82@159.203.210.80:30303",
"enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@10.6.6.117:30303", "enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@51.15.42.252:30303",
"enode://fe11ef89fc5ac9da358fc160857855f25bbf9e332c79b9ca7089330c02b728b2349988c6062f10982041702110745e203d26975a6b34bcc97144f9fe439034e8@10.1.72.117:30303" "enode://8d91c8137890d29110b9463882f17ae4e279cd2c90cf56573187ed1c8546fca5f590a9f05e9f108eb1bd91767ed01ede4daad9e001b61727885eaa246ddb39c2@163.172.171.38:30303"
], ],
"accounts": { "accounts": {
"0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, "0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },

View File

@ -0,0 +1,304 @@
{
"name": "Ropsten",
"engine": {
"Ethash": {
"params": {
"gasLimitBoundDivisor": "0x0400",
"minimumDifficulty": "0x020000",
"difficultyBoundDivisor": "0x0800",
"durationLimit": "0x0d",
"blockReward": "0x4563918244F40000",
"registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d",
"homesteadTransition": 0,
"eip150Transition": 0,
"eip155Transition": 10,
"eip160Transition": 10,
"eip161abcTransition": 10,
"eip161dTransition": 10
}
}
},
"params": {
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x3"
},
"genesis": {
"seal": {
"ethereum": {
"nonce": "0x0000000000000042",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"difficulty": "0x100000",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x3535353535353535353535353535353535353535353535353535353535353535",
"gasLimit": "0x1000000"
},
"nodes": [
"enode://a22f0977ce02653bf95e38730106356342df48b5222e2c2a1a6f9ef34769bf593bae9ca0a888cf60839edd52efc1b6e393c63a57d76f4c4fe14e641f1f9e637e@128.199.55.137:30303",
"enode://012239fccf3ff1d92b036983a430cb6705c6528c96c0354413f8854802138e5135c084ab36e7c54efb621c46728df8c3a6f4c1db9bb48a1330efe3f82f2dd7a6@52.169.94.142:30303"
],
"accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "0", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "0", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "0", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "0", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"0000000000000000000000000000000000000000": { "balance": "1" },
"0000000000000000000000000000000000000005": { "balance": "1" },
"0000000000000000000000000000000000000006": { "balance": "1" },
"0000000000000000000000000000000000000007": { "balance": "1" },
"0000000000000000000000000000000000000008": { "balance": "1" },
"0000000000000000000000000000000000000009": { "balance": "1" },
"000000000000000000000000000000000000000a": { "balance": "0" },
"000000000000000000000000000000000000000b": { "balance": "0" },
"000000000000000000000000000000000000000c": { "balance": "0" },
"000000000000000000000000000000000000000d": { "balance": "0" },
"000000000000000000000000000000000000000e": { "balance": "0" },
"000000000000000000000000000000000000000f": { "balance": "0" },
"0000000000000000000000000000000000000010": { "balance": "0" },
"0000000000000000000000000000000000000011": { "balance": "0" },
"0000000000000000000000000000000000000012": { "balance": "0" },
"0000000000000000000000000000000000000013": { "balance": "0" },
"0000000000000000000000000000000000000014": { "balance": "0" },
"0000000000000000000000000000000000000015": { "balance": "0" },
"0000000000000000000000000000000000000016": { "balance": "0" },
"0000000000000000000000000000000000000017": { "balance": "0" },
"0000000000000000000000000000000000000018": { "balance": "0" },
"0000000000000000000000000000000000000019": { "balance": "0" },
"000000000000000000000000000000000000001a": { "balance": "0" },
"000000000000000000000000000000000000001b": { "balance": "0" },
"000000000000000000000000000000000000001c": { "balance": "0" },
"000000000000000000000000000000000000001d": { "balance": "0" },
"000000000000000000000000000000000000001e": { "balance": "0" },
"000000000000000000000000000000000000001f": { "balance": "0" },
"0000000000000000000000000000000000000020": { "balance": "0" },
"0000000000000000000000000000000000000021": { "balance": "0" },
"0000000000000000000000000000000000000022": { "balance": "0" },
"0000000000000000000000000000000000000023": { "balance": "0" },
"0000000000000000000000000000000000000024": { "balance": "0" },
"0000000000000000000000000000000000000025": { "balance": "0" },
"0000000000000000000000000000000000000026": { "balance": "0" },
"0000000000000000000000000000000000000027": { "balance": "0" },
"0000000000000000000000000000000000000028": { "balance": "0" },
"0000000000000000000000000000000000000029": { "balance": "0" },
"000000000000000000000000000000000000002a": { "balance": "0" },
"000000000000000000000000000000000000002b": { "balance": "0" },
"000000000000000000000000000000000000002c": { "balance": "0" },
"000000000000000000000000000000000000002d": { "balance": "0" },
"000000000000000000000000000000000000002e": { "balance": "0" },
"000000000000000000000000000000000000002f": { "balance": "0" },
"0000000000000000000000000000000000000030": { "balance": "0" },
"0000000000000000000000000000000000000031": { "balance": "0" },
"0000000000000000000000000000000000000032": { "balance": "0" },
"0000000000000000000000000000000000000033": { "balance": "0" },
"0000000000000000000000000000000000000034": { "balance": "0" },
"0000000000000000000000000000000000000035": { "balance": "0" },
"0000000000000000000000000000000000000036": { "balance": "0" },
"0000000000000000000000000000000000000037": { "balance": "0" },
"0000000000000000000000000000000000000038": { "balance": "0" },
"0000000000000000000000000000000000000039": { "balance": "0" },
"000000000000000000000000000000000000003a": { "balance": "0" },
"000000000000000000000000000000000000003b": { "balance": "0" },
"000000000000000000000000000000000000003c": { "balance": "0" },
"000000000000000000000000000000000000003d": { "balance": "0" },
"000000000000000000000000000000000000003e": { "balance": "0" },
"000000000000000000000000000000000000003f": { "balance": "0" },
"0000000000000000000000000000000000000040": { "balance": "0" },
"0000000000000000000000000000000000000041": { "balance": "0" },
"0000000000000000000000000000000000000042": { "balance": "0" },
"0000000000000000000000000000000000000043": { "balance": "0" },
"0000000000000000000000000000000000000044": { "balance": "0" },
"0000000000000000000000000000000000000045": { "balance": "0" },
"0000000000000000000000000000000000000046": { "balance": "0" },
"0000000000000000000000000000000000000047": { "balance": "0" },
"0000000000000000000000000000000000000048": { "balance": "0" },
"0000000000000000000000000000000000000049": { "balance": "0" },
"000000000000000000000000000000000000004a": { "balance": "0" },
"000000000000000000000000000000000000004b": { "balance": "0" },
"000000000000000000000000000000000000004c": { "balance": "0" },
"000000000000000000000000000000000000004d": { "balance": "0" },
"000000000000000000000000000000000000004e": { "balance": "0" },
"000000000000000000000000000000000000004f": { "balance": "0" },
"0000000000000000000000000000000000000050": { "balance": "0" },
"0000000000000000000000000000000000000051": { "balance": "0" },
"0000000000000000000000000000000000000052": { "balance": "0" },
"0000000000000000000000000000000000000053": { "balance": "0" },
"0000000000000000000000000000000000000054": { "balance": "0" },
"0000000000000000000000000000000000000055": { "balance": "0" },
"0000000000000000000000000000000000000056": { "balance": "0" },
"0000000000000000000000000000000000000057": { "balance": "0" },
"0000000000000000000000000000000000000058": { "balance": "0" },
"0000000000000000000000000000000000000059": { "balance": "0" },
"000000000000000000000000000000000000005a": { "balance": "0" },
"000000000000000000000000000000000000005b": { "balance": "0" },
"000000000000000000000000000000000000005c": { "balance": "0" },
"000000000000000000000000000000000000005d": { "balance": "0" },
"000000000000000000000000000000000000005e": { "balance": "0" },
"000000000000000000000000000000000000005f": { "balance": "0" },
"0000000000000000000000000000000000000060": { "balance": "0" },
"0000000000000000000000000000000000000061": { "balance": "0" },
"0000000000000000000000000000000000000062": { "balance": "0" },
"0000000000000000000000000000000000000063": { "balance": "0" },
"0000000000000000000000000000000000000064": { "balance": "0" },
"0000000000000000000000000000000000000065": { "balance": "0" },
"0000000000000000000000000000000000000066": { "balance": "0" },
"0000000000000000000000000000000000000067": { "balance": "0" },
"0000000000000000000000000000000000000068": { "balance": "0" },
"0000000000000000000000000000000000000069": { "balance": "0" },
"000000000000000000000000000000000000006a": { "balance": "0" },
"000000000000000000000000000000000000006b": { "balance": "0" },
"000000000000000000000000000000000000006c": { "balance": "0" },
"000000000000000000000000000000000000006d": { "balance": "0" },
"000000000000000000000000000000000000006e": { "balance": "0" },
"000000000000000000000000000000000000006f": { "balance": "0" },
"0000000000000000000000000000000000000070": { "balance": "0" },
"0000000000000000000000000000000000000071": { "balance": "0" },
"0000000000000000000000000000000000000072": { "balance": "0" },
"0000000000000000000000000000000000000073": { "balance": "0" },
"0000000000000000000000000000000000000074": { "balance": "0" },
"0000000000000000000000000000000000000075": { "balance": "0" },
"0000000000000000000000000000000000000076": { "balance": "0" },
"0000000000000000000000000000000000000077": { "balance": "0" },
"0000000000000000000000000000000000000078": { "balance": "0" },
"0000000000000000000000000000000000000079": { "balance": "0" },
"000000000000000000000000000000000000007a": { "balance": "0" },
"000000000000000000000000000000000000007b": { "balance": "0" },
"000000000000000000000000000000000000007c": { "balance": "0" },
"000000000000000000000000000000000000007d": { "balance": "0" },
"000000000000000000000000000000000000007e": { "balance": "0" },
"000000000000000000000000000000000000007f": { "balance": "0" },
"0000000000000000000000000000000000000080": { "balance": "0" },
"0000000000000000000000000000000000000081": { "balance": "0" },
"0000000000000000000000000000000000000082": { "balance": "0" },
"0000000000000000000000000000000000000083": { "balance": "0" },
"0000000000000000000000000000000000000084": { "balance": "0" },
"0000000000000000000000000000000000000085": { "balance": "0" },
"0000000000000000000000000000000000000086": { "balance": "0" },
"0000000000000000000000000000000000000087": { "balance": "0" },
"0000000000000000000000000000000000000088": { "balance": "0" },
"0000000000000000000000000000000000000089": { "balance": "0" },
"000000000000000000000000000000000000008a": { "balance": "0" },
"000000000000000000000000000000000000008b": { "balance": "0" },
"000000000000000000000000000000000000008c": { "balance": "0" },
"000000000000000000000000000000000000008d": { "balance": "0" },
"000000000000000000000000000000000000008e": { "balance": "0" },
"000000000000000000000000000000000000008f": { "balance": "0" },
"0000000000000000000000000000000000000090": { "balance": "0" },
"0000000000000000000000000000000000000091": { "balance": "0" },
"0000000000000000000000000000000000000092": { "balance": "0" },
"0000000000000000000000000000000000000093": { "balance": "0" },
"0000000000000000000000000000000000000094": { "balance": "0" },
"0000000000000000000000000000000000000095": { "balance": "0" },
"0000000000000000000000000000000000000096": { "balance": "0" },
"0000000000000000000000000000000000000097": { "balance": "0" },
"0000000000000000000000000000000000000098": { "balance": "0" },
"0000000000000000000000000000000000000099": { "balance": "0" },
"000000000000000000000000000000000000009a": { "balance": "0" },
"000000000000000000000000000000000000009b": { "balance": "0" },
"000000000000000000000000000000000000009c": { "balance": "0" },
"000000000000000000000000000000000000009d": { "balance": "0" },
"000000000000000000000000000000000000009e": { "balance": "0" },
"000000000000000000000000000000000000009f": { "balance": "0" },
"00000000000000000000000000000000000000a0": { "balance": "0" },
"00000000000000000000000000000000000000a1": { "balance": "0" },
"00000000000000000000000000000000000000a2": { "balance": "0" },
"00000000000000000000000000000000000000a3": { "balance": "0" },
"00000000000000000000000000000000000000a4": { "balance": "0" },
"00000000000000000000000000000000000000a5": { "balance": "0" },
"00000000000000000000000000000000000000a6": { "balance": "0" },
"00000000000000000000000000000000000000a7": { "balance": "0" },
"00000000000000000000000000000000000000a8": { "balance": "0" },
"00000000000000000000000000000000000000a9": { "balance": "0" },
"00000000000000000000000000000000000000aa": { "balance": "0" },
"00000000000000000000000000000000000000ab": { "balance": "0" },
"00000000000000000000000000000000000000ac": { "balance": "0" },
"00000000000000000000000000000000000000ad": { "balance": "0" },
"00000000000000000000000000000000000000ae": { "balance": "0" },
"00000000000000000000000000000000000000af": { "balance": "0" },
"00000000000000000000000000000000000000b0": { "balance": "0" },
"00000000000000000000000000000000000000b1": { "balance": "0" },
"00000000000000000000000000000000000000b2": { "balance": "0" },
"00000000000000000000000000000000000000b3": { "balance": "0" },
"00000000000000000000000000000000000000b4": { "balance": "0" },
"00000000000000000000000000000000000000b5": { "balance": "0" },
"00000000000000000000000000000000000000b6": { "balance": "0" },
"00000000000000000000000000000000000000b7": { "balance": "0" },
"00000000000000000000000000000000000000b8": { "balance": "0" },
"00000000000000000000000000000000000000b9": { "balance": "0" },
"00000000000000000000000000000000000000ba": { "balance": "0" },
"00000000000000000000000000000000000000bb": { "balance": "0" },
"00000000000000000000000000000000000000bc": { "balance": "0" },
"00000000000000000000000000000000000000bd": { "balance": "0" },
"00000000000000000000000000000000000000be": { "balance": "0" },
"00000000000000000000000000000000000000bf": { "balance": "0" },
"00000000000000000000000000000000000000c0": { "balance": "0" },
"00000000000000000000000000000000000000c1": { "balance": "0" },
"00000000000000000000000000000000000000c2": { "balance": "0" },
"00000000000000000000000000000000000000c3": { "balance": "0" },
"00000000000000000000000000000000000000c4": { "balance": "0" },
"00000000000000000000000000000000000000c5": { "balance": "0" },
"00000000000000000000000000000000000000c6": { "balance": "0" },
"00000000000000000000000000000000000000c7": { "balance": "0" },
"00000000000000000000000000000000000000c8": { "balance": "0" },
"00000000000000000000000000000000000000c9": { "balance": "0" },
"00000000000000000000000000000000000000ca": { "balance": "0" },
"00000000000000000000000000000000000000cb": { "balance": "0" },
"00000000000000000000000000000000000000cc": { "balance": "0" },
"00000000000000000000000000000000000000cd": { "balance": "0" },
"00000000000000000000000000000000000000ce": { "balance": "0" },
"00000000000000000000000000000000000000cf": { "balance": "0" },
"00000000000000000000000000000000000000d0": { "balance": "0" },
"00000000000000000000000000000000000000d1": { "balance": "0" },
"00000000000000000000000000000000000000d2": { "balance": "0" },
"00000000000000000000000000000000000000d3": { "balance": "0" },
"00000000000000000000000000000000000000d4": { "balance": "0" },
"00000000000000000000000000000000000000d5": { "balance": "0" },
"00000000000000000000000000000000000000d6": { "balance": "0" },
"00000000000000000000000000000000000000d7": { "balance": "0" },
"00000000000000000000000000000000000000d8": { "balance": "0" },
"00000000000000000000000000000000000000d9": { "balance": "0" },
"00000000000000000000000000000000000000da": { "balance": "0" },
"00000000000000000000000000000000000000db": { "balance": "0" },
"00000000000000000000000000000000000000dc": { "balance": "0" },
"00000000000000000000000000000000000000dd": { "balance": "0" },
"00000000000000000000000000000000000000de": { "balance": "0" },
"00000000000000000000000000000000000000df": { "balance": "0" },
"00000000000000000000000000000000000000e0": { "balance": "0" },
"00000000000000000000000000000000000000e1": { "balance": "0" },
"00000000000000000000000000000000000000e2": { "balance": "0" },
"00000000000000000000000000000000000000e3": { "balance": "0" },
"00000000000000000000000000000000000000e4": { "balance": "0" },
"00000000000000000000000000000000000000e5": { "balance": "0" },
"00000000000000000000000000000000000000e6": { "balance": "0" },
"00000000000000000000000000000000000000e7": { "balance": "0" },
"00000000000000000000000000000000000000e8": { "balance": "0" },
"00000000000000000000000000000000000000e9": { "balance": "0" },
"00000000000000000000000000000000000000ea": { "balance": "0" },
"00000000000000000000000000000000000000eb": { "balance": "0" },
"00000000000000000000000000000000000000ec": { "balance": "0" },
"00000000000000000000000000000000000000ed": { "balance": "0" },
"00000000000000000000000000000000000000ee": { "balance": "0" },
"00000000000000000000000000000000000000ef": { "balance": "0" },
"00000000000000000000000000000000000000f0": { "balance": "0" },
"00000000000000000000000000000000000000f1": { "balance": "0" },
"00000000000000000000000000000000000000f2": { "balance": "0" },
"00000000000000000000000000000000000000f3": { "balance": "0" },
"00000000000000000000000000000000000000f4": { "balance": "0" },
"00000000000000000000000000000000000000f5": { "balance": "0" },
"00000000000000000000000000000000000000f6": { "balance": "0" },
"00000000000000000000000000000000000000f7": { "balance": "0" },
"00000000000000000000000000000000000000f8": { "balance": "0" },
"00000000000000000000000000000000000000f9": { "balance": "0" },
"00000000000000000000000000000000000000fa": { "balance": "0" },
"00000000000000000000000000000000000000fb": { "balance": "0" },
"00000000000000000000000000000000000000fc": { "balance": "0" },
"00000000000000000000000000000000000000fd": { "balance": "0" },
"00000000000000000000000000000000000000fe": { "balance": "0" },
"00000000000000000000000000000000000000ff": { "balance": "0" },
"874b54a8bd152966d63f706bae1ffeb0411921e5": { "balance": "1000000000000000000000000000000" }
}
}

View File

@ -892,12 +892,9 @@ impl BlockChainClient for Client {
let mut mode = self.mode.lock(); let mut mode = self.mode.lock();
*mode = new_mode.clone().into(); *mode = new_mode.clone().into();
trace!(target: "mode", "Mode now {:?}", &*mode); trace!(target: "mode", "Mode now {:?}", &*mode);
match *self.on_mode_change.lock() { if let Some(ref mut f) = *self.on_mode_change.lock() {
Some(ref mut f) => {
trace!(target: "mode", "Making callback..."); trace!(target: "mode", "Making callback...");
f(&*mode) f(&*mode)
},
_ => {}
} }
} }
match new_mode { match new_mode {

View File

@ -63,6 +63,9 @@ pub fn new_transition_test() -> Spec { load(include_bytes!("../../res/ethereum/t
/// Create a new Frontier main net chain spec without genesis accounts. /// Create a new Frontier main net chain spec without genesis accounts.
pub fn new_mainnet_like() -> Spec { load(include_bytes!("../../res/ethereum/frontier_like_test.json")) } pub fn new_mainnet_like() -> Spec { load(include_bytes!("../../res/ethereum/frontier_like_test.json")) }
/// Create a new Ropsten chain spec.
pub fn new_ropsten() -> Spec { load(include_bytes!("../../res/ethereum/ropsten.json")) }
/// Create a new Morden chain spec. /// Create a new Morden chain spec.
pub fn new_morden() -> Spec { load(include_bytes!("../../res/ethereum/morden.json")) } pub fn new_morden() -> Spec { load(include_bytes!("../../res/ethereum/morden.json")) }

View File

@ -103,6 +103,7 @@ extern crate ethcore_bloom_journal as bloom_journal;
extern crate byteorder; extern crate byteorder;
extern crate transient_hashmap; extern crate transient_hashmap;
extern crate ethcore_network as network; extern crate ethcore_network as network;
extern crate linked_hash_map;
#[macro_use] #[macro_use]
extern crate log; extern crate log;

View File

@ -0,0 +1,196 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Local Transactions List.
use linked_hash_map::LinkedHashMap;
use transaction::SignedTransaction;
use error::TransactionError;
use util::{U256, H256};
/// Status of local transaction.
/// Can indicate that the transaction is currently part of the queue (`Pending/Future`)
/// or gives a reason why the transaction was removed.
#[derive(Debug, PartialEq, Clone)]
pub enum Status {
/// The transaction is currently in the transaction queue.
Pending,
/// The transaction is in future part of the queue.
Future,
/// Transaction is already mined.
Mined(SignedTransaction),
/// Transaction is dropped because of limit
Dropped(SignedTransaction),
/// Replaced because of higher gas price of another transaction.
Replaced(SignedTransaction, U256, H256),
/// Transaction was never accepted to the queue.
Rejected(SignedTransaction, TransactionError),
/// Transaction is invalid.
Invalid(SignedTransaction),
}
impl Status {
fn is_current(&self) -> bool {
*self == Status::Pending || *self == Status::Future
}
}
/// Keeps track of local transactions that are in the queue or were mined/dropped recently.
#[derive(Debug)]
pub struct LocalTransactionsList {
max_old: usize,
transactions: LinkedHashMap<H256, Status>,
}
impl Default for LocalTransactionsList {
fn default() -> Self {
Self::new(10)
}
}
impl LocalTransactionsList {
pub fn new(max_old: usize) -> Self {
LocalTransactionsList {
max_old: max_old,
transactions: Default::default(),
}
}
pub fn mark_pending(&mut self, hash: H256) {
self.clear_old();
self.transactions.insert(hash, Status::Pending);
}
pub fn mark_future(&mut self, hash: H256) {
self.transactions.insert(hash, Status::Future);
self.clear_old();
}
pub fn mark_rejected(&mut self, tx: SignedTransaction, err: TransactionError) {
self.transactions.insert(tx.hash(), Status::Rejected(tx, err));
self.clear_old();
}
pub fn mark_replaced(&mut self, tx: SignedTransaction, gas_price: U256, hash: H256) {
self.transactions.insert(tx.hash(), Status::Replaced(tx, gas_price, hash));
self.clear_old();
}
pub fn mark_invalid(&mut self, tx: SignedTransaction) {
self.transactions.insert(tx.hash(), Status::Invalid(tx));
self.clear_old();
}
pub fn mark_dropped(&mut self, tx: SignedTransaction) {
self.transactions.insert(tx.hash(), Status::Dropped(tx));
self.clear_old();
}
pub fn mark_mined(&mut self, tx: SignedTransaction) {
self.transactions.insert(tx.hash(), Status::Mined(tx));
self.clear_old();
}
pub fn contains(&self, hash: &H256) -> bool {
self.transactions.contains_key(hash)
}
pub fn all_transactions(&self) -> &LinkedHashMap<H256, Status> {
&self.transactions
}
fn clear_old(&mut self) {
let number_of_old = self.transactions
.values()
.filter(|status| !status.is_current())
.count();
if self.max_old >= number_of_old {
return;
}
let to_remove = self.transactions
.iter()
.filter(|&(_, status)| !status.is_current())
.map(|(hash, _)| *hash)
.take(number_of_old - self.max_old)
.collect::<Vec<_>>();
for hash in to_remove {
self.transactions.remove(&hash);
}
}
}
#[cfg(test)]
mod tests {
use util::U256;
use ethkey::{Random, Generator};
use transaction::{Action, Transaction, SignedTransaction};
use super::{LocalTransactionsList, Status};
#[test]
fn should_add_transaction_as_pending() {
// given
let mut list = LocalTransactionsList::default();
// when
list.mark_pending(10.into());
list.mark_future(20.into());
// then
assert!(list.contains(&10.into()), "Should contain the transaction.");
assert!(list.contains(&20.into()), "Should contain the transaction.");
let statuses = list.all_transactions().values().cloned().collect::<Vec<Status>>();
assert_eq!(statuses, vec![Status::Pending, Status::Future]);
}
#[test]
fn should_clear_old_transactions() {
// given
let mut list = LocalTransactionsList::new(1);
let tx1 = new_tx(10.into());
let tx1_hash = tx1.hash();
let tx2 = new_tx(50.into());
let tx2_hash = tx2.hash();
list.mark_pending(10.into());
list.mark_invalid(tx1);
list.mark_dropped(tx2);
assert!(list.contains(&tx2_hash));
assert!(!list.contains(&tx1_hash));
assert!(list.contains(&10.into()));
// when
list.mark_future(15.into());
// then
assert!(list.contains(&10.into()));
assert!(list.contains(&15.into()));
}
fn new_tx(nonce: U256) -> SignedTransaction {
let keypair = Random.generate().unwrap();
Transaction {
action: Action::Create,
value: U256::from(100),
data: Default::default(),
gas: U256::from(10),
gas_price: U256::from(1245),
nonce: nonce
}.sign(keypair.secret(), None)
}
}

View File

@ -21,8 +21,10 @@ use util::*;
use util::using_queue::{UsingQueue, GetAction}; use util::using_queue::{UsingQueue, GetAction};
use account_provider::AccountProvider; use account_provider::AccountProvider;
use views::{BlockView, HeaderView}; use views::{BlockView, HeaderView};
use header::Header;
use state::{State, CleanupMode}; use state::{State, CleanupMode};
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics}; use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics};
use client::TransactionImportResult;
use executive::contract_address; use executive::contract_address;
use block::{ClosedBlock, SealedBlock, IsBlock, Block}; use block::{ClosedBlock, SealedBlock, IsBlock, Block};
use error::*; use error::*;
@ -33,8 +35,8 @@ use engines::Engine;
use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin}; use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
use miner::banning_queue::{BanningTransactionQueue, Threshold}; use miner::banning_queue::{BanningTransactionQueue, Threshold};
use miner::work_notify::WorkPoster; use miner::work_notify::WorkPoster;
use client::TransactionImportResult;
use miner::price_info::PriceInfo; use miner::price_info::PriceInfo;
use miner::local_transactions::{Status as LocalTransactionStatus};
use header::BlockNumber; use header::BlockNumber;
/// Different possible definitions for pending transaction set. /// Different possible definitions for pending transaction set.
@ -562,7 +564,7 @@ impl Miner {
prepare_new prepare_new
} }
fn add_transactions_to_queue(&self, chain: &MiningBlockChainClient, transactions: Vec<SignedTransaction>, origin: TransactionOrigin, transaction_queue: &mut BanningTransactionQueue) -> fn add_transactions_to_queue(&self, chain: &MiningBlockChainClient, transactions: Vec<SignedTransaction>, default_origin: TransactionOrigin, transaction_queue: &mut BanningTransactionQueue) ->
Vec<Result<TransactionImportResult, Error>> { Vec<Result<TransactionImportResult, Error>> {
let fetch_account = |a: &Address| AccountDetails { let fetch_account = |a: &Address| AccountDetails {
@ -570,16 +572,38 @@ impl Miner {
balance: chain.latest_balance(a), balance: chain.latest_balance(a),
}; };
let accounts = self.accounts.as_ref()
.and_then(|provider| provider.accounts().ok())
.map(|accounts| accounts.into_iter().collect::<HashSet<_>>());
let schedule = chain.latest_schedule(); let schedule = chain.latest_schedule();
let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into(); let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into();
let best_block_header: Header = ::rlp::decode(&chain.best_block_header());
transactions.into_iter() transactions.into_iter()
.map(|tx| match origin { .filter(|tx| match self.engine.verify_transaction_basic(tx, &best_block_header) {
Ok(()) => true,
Err(e) => {
debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e);
false
}
}
)
.map(|tx| {
let origin = accounts.as_ref().and_then(|accounts| {
tx.sender().ok().and_then(|sender| match accounts.contains(&sender) {
true => Some(TransactionOrigin::Local),
false => None,
})
}).unwrap_or(default_origin);
match origin {
TransactionOrigin::Local | TransactionOrigin::RetractedBlock => { TransactionOrigin::Local | TransactionOrigin::RetractedBlock => {
transaction_queue.add(tx, origin, &fetch_account, &gas_required) transaction_queue.add(tx, origin, &fetch_account, &gas_required)
}, },
TransactionOrigin::External => { TransactionOrigin::External => {
transaction_queue.add_with_banlist(tx, &fetch_account, &gas_required) transaction_queue.add_with_banlist(tx, &fetch_account, &gas_required)
} }
}
}) })
.collect() .collect()
} }
@ -853,6 +877,14 @@ impl MinerService for Miner {
queue.top_transactions() queue.top_transactions()
} }
fn local_transactions(&self) -> BTreeMap<H256, LocalTransactionStatus> {
let queue = self.transaction_queue.lock();
queue.local_transactions()
.iter()
.map(|(hash, status)| (*hash, status.clone()))
.collect()
}
fn pending_transactions(&self, best_block: BlockNumber) -> Vec<SignedTransaction> { fn pending_transactions(&self, best_block: BlockNumber) -> Vec<SignedTransaction> {
let queue = self.transaction_queue.lock(); let queue = self.transaction_queue.lock();
match self.options.pending_set { match self.options.pending_set {

View File

@ -41,16 +41,18 @@
//! } //! }
//! ``` //! ```
mod miner;
mod external;
mod transaction_queue;
mod banning_queue; mod banning_queue;
mod work_notify; mod external;
mod local_transactions;
mod miner;
mod price_info; mod price_info;
mod transaction_queue;
mod work_notify;
pub use self::transaction_queue::{TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
pub use self::miner::{Miner, MinerOptions, Banning, PendingSet, GasPricer, GasPriceCalibratorOptions, GasLimit};
pub use self::external::{ExternalMiner, ExternalMinerService}; pub use self::external::{ExternalMiner, ExternalMinerService};
pub use self::miner::{Miner, MinerOptions, Banning, PendingSet, GasPricer, GasPriceCalibratorOptions, GasLimit};
pub use self::transaction_queue::{TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
pub use self::local_transactions::{Status as LocalTransactionStatus};
pub use client::TransactionImportResult; pub use client::TransactionImportResult;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -145,6 +147,9 @@ pub trait MinerService : Send + Sync {
/// Get a list of all pending transactions. /// Get a list of all pending transactions.
fn pending_transactions(&self, best_block: BlockNumber) -> Vec<SignedTransaction>; fn pending_transactions(&self, best_block: BlockNumber) -> Vec<SignedTransaction>;
/// Get a list of local transactions with statuses.
fn local_transactions(&self) -> BTreeMap<H256, LocalTransactionStatus>;
/// Get a list of all pending receipts. /// Get a list of all pending receipts.
fn pending_receipts(&self, best_block: BlockNumber) -> BTreeMap<H256, Receipt>; fn pending_receipts(&self, best_block: BlockNumber) -> BTreeMap<H256, Receipt>;

View File

@ -86,11 +86,13 @@ use std::ops::Deref;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::cmp; use std::cmp;
use std::collections::{HashSet, HashMap, BTreeSet, BTreeMap}; use std::collections::{HashSet, HashMap, BTreeSet, BTreeMap};
use linked_hash_map::LinkedHashMap;
use util::{Address, H256, Uint, U256}; use util::{Address, H256, Uint, U256};
use util::table::Table; use util::table::Table;
use transaction::*; use transaction::*;
use error::{Error, TransactionError}; use error::{Error, TransactionError};
use client::TransactionImportResult; use client::TransactionImportResult;
use miner::local_transactions::{LocalTransactionsList, Status as LocalTransactionStatus};
/// Transaction origin /// Transaction origin
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -125,6 +127,12 @@ impl Ord for TransactionOrigin {
} }
} }
impl TransactionOrigin {
fn is_local(&self) -> bool {
*self == TransactionOrigin::Local
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// Light structure used to identify transaction and its order /// Light structure used to identify transaction and its order
struct TransactionOrder { struct TransactionOrder {
@ -201,17 +209,16 @@ impl Ord for TransactionOrder {
return self.penalties.cmp(&b.penalties); return self.penalties.cmp(&b.penalties);
} }
// First check nonce_height
if self.nonce_height != b.nonce_height {
return self.nonce_height.cmp(&b.nonce_height);
}
// Local transactions should always have priority // Local transactions should always have priority
// NOTE nonce has to be checked first, cause otherwise the order might be wrong.
if self.origin != b.origin { if self.origin != b.origin {
return self.origin.cmp(&b.origin); return self.origin.cmp(&b.origin);
} }
// Check nonce_height
if self.nonce_height != b.nonce_height {
return self.nonce_height.cmp(&b.nonce_height);
}
match self.strategy { match self.strategy {
PrioritizationStrategy::GasAndGasPrice => { PrioritizationStrategy::GasAndGasPrice => {
if self.gas != b.gas { if self.gas != b.gas {
@ -242,6 +249,7 @@ impl Ord for TransactionOrder {
} }
/// Verified transaction (with sender) /// Verified transaction (with sender)
#[derive(Debug)]
struct VerifiedTransaction { struct VerifiedTransaction {
/// Transaction /// Transaction
transaction: SignedTransaction, transaction: SignedTransaction,
@ -352,7 +360,7 @@ impl TransactionSet {
/// ///
/// It drops transactions from this set but also removes associated `VerifiedTransaction`. /// It drops transactions from this set but also removes associated `VerifiedTransaction`.
/// Returns addresses and lowest nonces of transactions removed because of limit. /// Returns addresses and lowest nonces of transactions removed because of limit.
fn enforce_limit(&mut self, by_hash: &mut HashMap<H256, VerifiedTransaction>) -> Option<HashMap<Address, U256>> { fn enforce_limit(&mut self, by_hash: &mut HashMap<H256, VerifiedTransaction>, local: &mut LocalTransactionsList) -> Option<HashMap<Address, U256>> {
let mut count = 0; let mut count = 0;
let mut gas: U256 = 0.into(); let mut gas: U256 = 0.into();
let to_drop : Vec<(Address, U256)> = { let to_drop : Vec<(Address, U256)> = {
@ -379,9 +387,13 @@ impl TransactionSet {
.expect("Transaction has just been found in `by_priority`; so it is in `by_address` also."); .expect("Transaction has just been found in `by_priority`; so it is in `by_address` also.");
trace!(target: "txqueue", "Dropped out of limit transaction: {:?}", order.hash); trace!(target: "txqueue", "Dropped out of limit transaction: {:?}", order.hash);
by_hash.remove(&order.hash) let order = by_hash.remove(&order.hash)
.expect("hash is in `by_priorty`; all hashes in `by_priority` must be in `by_hash`; qed"); .expect("hash is in `by_priorty`; all hashes in `by_priority` must be in `by_hash`; qed");
if order.origin.is_local() {
local.mark_dropped(order.transaction);
}
let min = removed.get(&sender).map_or(nonce, |val| cmp::min(*val, nonce)); let min = removed.get(&sender).map_or(nonce, |val| cmp::min(*val, nonce));
removed.insert(sender, min); removed.insert(sender, min);
removed removed
@ -488,6 +500,8 @@ pub struct TransactionQueue {
by_hash: HashMap<H256, VerifiedTransaction>, by_hash: HashMap<H256, VerifiedTransaction>,
/// Last nonce of transaction in current (to quickly check next expected transaction) /// Last nonce of transaction in current (to quickly check next expected transaction)
last_nonces: HashMap<Address, U256>, last_nonces: HashMap<Address, U256>,
/// List of local transactions and their statuses.
local_transactions: LocalTransactionsList,
} }
impl Default for TransactionQueue { impl Default for TransactionQueue {
@ -529,6 +543,7 @@ impl TransactionQueue {
future: future, future: future,
by_hash: HashMap::new(), by_hash: HashMap::new(),
last_nonces: HashMap::new(), last_nonces: HashMap::new(),
local_transactions: LocalTransactionsList::default(),
} }
} }
@ -537,8 +552,8 @@ impl TransactionQueue {
self.current.set_limit(limit); self.current.set_limit(limit);
self.future.set_limit(limit); self.future.set_limit(limit);
// And ensure the limits // And ensure the limits
self.current.enforce_limit(&mut self.by_hash); self.current.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
self.future.enforce_limit(&mut self.by_hash); self.future.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
} }
/// Returns current limit of transactions in the queue. /// Returns current limit of transactions in the queue.
@ -578,7 +593,7 @@ impl TransactionQueue {
pub fn set_total_gas_limit(&mut self, gas_limit: U256) { pub fn set_total_gas_limit(&mut self, gas_limit: U256) {
self.future.gas_limit = gas_limit; self.future.gas_limit = gas_limit;
self.current.gas_limit = gas_limit; self.current.gas_limit = gas_limit;
self.future.enforce_limit(&mut self.by_hash); self.future.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
} }
/// Set the new limit for the amount of gas any individual transaction may have. /// Set the new limit for the amount of gas any individual transaction may have.
@ -609,6 +624,46 @@ impl TransactionQueue {
F: Fn(&Address) -> AccountDetails, F: Fn(&Address) -> AccountDetails,
G: Fn(&SignedTransaction) -> U256, G: Fn(&SignedTransaction) -> U256,
{ {
if origin == TransactionOrigin::Local {
let hash = tx.hash();
let cloned_tx = tx.clone();
let result = self.add_internal(tx, origin, fetch_account, gas_estimator);
match result {
Ok(TransactionImportResult::Current) => {
self.local_transactions.mark_pending(hash);
},
Ok(TransactionImportResult::Future) => {
self.local_transactions.mark_future(hash);
},
Err(Error::Transaction(ref err)) => {
// Sometimes transactions are re-imported, so
// don't overwrite transactions if they are already on the list
if !self.local_transactions.contains(&cloned_tx.hash()) {
self.local_transactions.mark_rejected(cloned_tx, err.clone());
}
},
Err(_) => {
self.local_transactions.mark_invalid(cloned_tx);
},
}
result
} else {
self.add_internal(tx, origin, fetch_account, gas_estimator)
}
}
/// Adds signed transaction to the queue.
fn add_internal<F, G>(
&mut self,
tx: SignedTransaction,
origin: TransactionOrigin,
fetch_account: &F,
gas_estimator: &G,
) -> Result<TransactionImportResult, Error> where
F: Fn(&Address) -> AccountDetails,
G: Fn(&SignedTransaction) -> U256,
{
if tx.gas_price < self.minimal_gas_price && origin != TransactionOrigin::Local { if tx.gas_price < self.minimal_gas_price && origin != TransactionOrigin::Local {
trace!(target: "txqueue", trace!(target: "txqueue",
@ -647,7 +702,6 @@ impl TransactionQueue {
self.gas_limit, self.gas_limit,
self.tx_gas_limit self.tx_gas_limit
); );
return Err(Error::Transaction(TransactionError::GasLimitExceeded { return Err(Error::Transaction(TransactionError::GasLimitExceeded {
limit: self.gas_limit, limit: self.gas_limit,
got: tx.gas, got: tx.gas,
@ -722,6 +776,12 @@ impl TransactionQueue {
None => return, None => return,
Some(t) => t, Some(t) => t,
}; };
// Never penalize local transactions
if transaction.origin.is_local() {
return;
}
let sender = transaction.sender(); let sender = transaction.sender();
// Penalize all transactions from this sender // Penalize all transactions from this sender
@ -766,6 +826,11 @@ impl TransactionQueue {
trace!(target: "txqueue", "Removing invalid transaction: {:?}", transaction.hash()); trace!(target: "txqueue", "Removing invalid transaction: {:?}", transaction.hash());
// Mark in locals
if self.local_transactions.contains(transaction_hash) {
self.local_transactions.mark_invalid(transaction.transaction.clone());
}
// Remove from future // Remove from future
let order = self.future.drop(&sender, &nonce); let order = self.future.drop(&sender, &nonce);
if order.is_some() { if order.is_some() {
@ -788,6 +853,33 @@ impl TransactionQueue {
} }
} }
/// Marks all transactions from particular sender as local transactions
fn mark_transactions_local(&mut self, sender: &Address) {
fn mark_local<F: FnMut(H256)>(sender: &Address, set: &mut TransactionSet, mut mark: F) {
// Mark all transactions from this sender as local
let nonces_from_sender = set.by_address.row(sender)
.map(|row_map| {
row_map.iter().filter_map(|(nonce, order)| if order.origin.is_local() {
None
} else {
Some(*nonce)
}).collect::<Vec<U256>>()
})
.unwrap_or_else(Vec::new);
for k in nonces_from_sender {
let mut order = set.drop(sender, &k).expect("transaction known to be in self.current/self.future; qed");
order.origin = TransactionOrigin::Local;
mark(order.hash);
set.insert(*sender, k, order);
}
}
let local = &mut self.local_transactions;
mark_local(sender, &mut self.current, |hash| local.mark_pending(hash));
mark_local(sender, &mut self.future, |hash| local.mark_future(hash));
}
/// Update height of all transactions in future transactions set. /// Update height of all transactions in future transactions set.
fn update_future(&mut self, sender: &Address, current_nonce: U256) { fn update_future(&mut self, sender: &Address, current_nonce: U256) {
// We need to drain all transactions for current sender from future and reinsert them with updated height // We need to drain all transactions for current sender from future and reinsert them with updated height
@ -821,15 +913,21 @@ impl TransactionQueue {
qed"); qed");
if k >= current_nonce { if k >= current_nonce {
let order = order.update_height(k, current_nonce); let order = order.update_height(k, current_nonce);
if order.origin.is_local() {
self.local_transactions.mark_future(order.hash);
}
if let Some(old) = self.future.insert(*sender, k, order.clone()) { if let Some(old) = self.future.insert(*sender, k, order.clone()) {
Self::replace_orders(*sender, k, old, order, &mut self.future, &mut self.by_hash); Self::replace_orders(*sender, k, old, order, &mut self.future, &mut self.by_hash, &mut self.local_transactions);
} }
} else { } else {
trace!(target: "txqueue", "Removing old transaction: {:?} (nonce: {} < {})", order.hash, k, current_nonce); trace!(target: "txqueue", "Removing old transaction: {:?} (nonce: {} < {})", order.hash, k, current_nonce);
self.by_hash.remove(&order.hash).expect("All transactions in `future` are also in `by_hash`"); let tx = self.by_hash.remove(&order.hash).expect("All transactions in `future` are also in `by_hash`");
if tx.origin.is_local() {
self.local_transactions.mark_mined(tx.transaction);
} }
} }
self.future.enforce_limit(&mut self.by_hash); }
self.future.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
} }
/// Returns top transactions from the queue ordered by priority. /// Returns top transactions from the queue ordered by priority.
@ -841,6 +939,11 @@ impl TransactionQueue {
.collect() .collect()
} }
/// Returns local transactions (some of them might not be part of the queue anymore).
pub fn local_transactions(&self) -> &LinkedHashMap<H256, LocalTransactionStatus> {
self.local_transactions.all_transactions()
}
#[cfg(test)] #[cfg(test)]
fn future_transactions(&self) -> Vec<SignedTransaction> { fn future_transactions(&self) -> Vec<SignedTransaction> {
self.future.by_priority self.future.by_priority
@ -897,8 +1000,11 @@ impl TransactionQueue {
self.future.by_gas_price.remove(&order.gas_price, &order.hash); self.future.by_gas_price.remove(&order.gas_price, &order.hash);
// Put to current // Put to current
let order = order.update_height(current_nonce, first_nonce); let order = order.update_height(current_nonce, first_nonce);
if order.origin.is_local() {
self.local_transactions.mark_pending(order.hash);
}
if let Some(old) = self.current.insert(address, current_nonce, order.clone()) { if let Some(old) = self.current.insert(address, current_nonce, order.clone()) {
Self::replace_orders(address, current_nonce, old, order, &mut self.current, &mut self.by_hash); Self::replace_orders(address, current_nonce, old, order, &mut self.current, &mut self.by_hash, &mut self.local_transactions);
} }
update_last_nonce_to = Some(current_nonce); update_last_nonce_to = Some(current_nonce);
current_nonce = current_nonce + U256::one(); current_nonce = current_nonce + U256::one();
@ -953,13 +1059,19 @@ impl TransactionQueue {
.cloned() .cloned()
.map_or(state_nonce, |n| n + U256::one()); .map_or(state_nonce, |n| n + U256::one());
if tx.origin.is_local() {
self.mark_transactions_local(&address);
}
// Future transaction // Future transaction
if nonce > next_nonce { if nonce > next_nonce {
// We have a gap - put to future. // We have a gap - put to future.
// Insert transaction (or replace old one with lower gas price) // Insert transaction (or replace old one with lower gas price)
try!(check_too_cheap(Self::replace_transaction(tx, state_nonce, min_gas_price, &mut self.future, &mut self.by_hash))); try!(check_too_cheap(
Self::replace_transaction(tx, state_nonce, min_gas_price, &mut self.future, &mut self.by_hash, &mut self.local_transactions)
));
// Enforce limit in Future // Enforce limit in Future
let removed = self.future.enforce_limit(&mut self.by_hash); let removed = self.future.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
// Return an error if this transaction was not imported because of limit. // Return an error if this transaction was not imported because of limit.
try!(check_if_removed(&address, &nonce, removed)); try!(check_if_removed(&address, &nonce, removed));
@ -973,13 +1085,15 @@ impl TransactionQueue {
self.move_matching_future_to_current(address, nonce + U256::one(), state_nonce); self.move_matching_future_to_current(address, nonce + U256::one(), state_nonce);
// Replace transaction if any // Replace transaction if any
try!(check_too_cheap(Self::replace_transaction(tx, state_nonce, min_gas_price, &mut self.current, &mut self.by_hash))); try!(check_too_cheap(
Self::replace_transaction(tx, state_nonce, min_gas_price, &mut self.current, &mut self.by_hash, &mut self.local_transactions)
));
// Keep track of highest nonce stored in current // Keep track of highest nonce stored in current
let new_max = self.last_nonces.get(&address).map_or(nonce, |n| cmp::max(nonce, *n)); let new_max = self.last_nonces.get(&address).map_or(nonce, |n| cmp::max(nonce, *n));
self.last_nonces.insert(address, new_max); self.last_nonces.insert(address, new_max);
// Also enforce the limit // Also enforce the limit
let removed = self.current.enforce_limit(&mut self.by_hash); let removed = self.current.enforce_limit(&mut self.by_hash, &mut self.local_transactions);
// If some transaction were removed because of limit we need to update last_nonces also. // If some transaction were removed because of limit we need to update last_nonces also.
self.update_last_nonces(&removed); self.update_last_nonces(&removed);
// Trigger error if the transaction we are importing was removed. // Trigger error if the transaction we are importing was removed.
@ -1010,7 +1124,14 @@ impl TransactionQueue {
/// ///
/// Returns `true` if transaction actually got to the queue (`false` if there was already a transaction with higher /// Returns `true` if transaction actually got to the queue (`false` if there was already a transaction with higher
/// gas_price) /// gas_price)
fn replace_transaction(tx: VerifiedTransaction, base_nonce: U256, min_gas_price: (U256, PrioritizationStrategy), set: &mut TransactionSet, by_hash: &mut HashMap<H256, VerifiedTransaction>) -> bool { fn replace_transaction(
tx: VerifiedTransaction,
base_nonce: U256,
min_gas_price: (U256, PrioritizationStrategy),
set: &mut TransactionSet,
by_hash: &mut HashMap<H256, VerifiedTransaction>,
local: &mut LocalTransactionsList,
) -> bool {
let order = TransactionOrder::for_transaction(&tx, base_nonce, min_gas_price.0, min_gas_price.1); let order = TransactionOrder::for_transaction(&tx, base_nonce, min_gas_price.0, min_gas_price.1);
let hash = tx.hash(); let hash = tx.hash();
let address = tx.sender(); let address = tx.sender();
@ -1019,16 +1140,27 @@ impl TransactionQueue {
let old_hash = by_hash.insert(hash, tx); let old_hash = by_hash.insert(hash, tx);
assert!(old_hash.is_none(), "Each hash has to be inserted exactly once."); assert!(old_hash.is_none(), "Each hash has to be inserted exactly once.");
trace!(target: "txqueue", "Inserting: {:?}", order);
if let Some(old) = set.insert(address, nonce, order.clone()) { if let Some(old) = set.insert(address, nonce, order.clone()) {
Self::replace_orders(address, nonce, old, order, set, by_hash) Self::replace_orders(address, nonce, old, order, set, by_hash, local)
} else { } else {
true true
} }
} }
fn replace_orders(address: Address, nonce: U256, old: TransactionOrder, order: TransactionOrder, set: &mut TransactionSet, by_hash: &mut HashMap<H256, VerifiedTransaction>) -> bool { fn replace_orders(
address: Address,
nonce: U256,
old: TransactionOrder,
order: TransactionOrder,
set: &mut TransactionSet,
by_hash: &mut HashMap<H256, VerifiedTransaction>,
local: &mut LocalTransactionsList,
) -> bool {
// There was already transaction in queue. Let's check which one should stay // There was already transaction in queue. Let's check which one should stay
let old_hash = old.hash;
let new_hash = order.hash;
let old_fee = old.gas_price; let old_fee = old.gas_price;
let new_fee = order.gas_price; let new_fee = order.gas_price;
if old_fee.cmp(&new_fee) == Ordering::Greater { if old_fee.cmp(&new_fee) == Ordering::Greater {
@ -1036,12 +1168,18 @@ impl TransactionQueue {
// Put back old transaction since it has greater priority (higher gas_price) // Put back old transaction since it has greater priority (higher gas_price)
set.insert(address, nonce, old); set.insert(address, nonce, old);
// and remove new one // and remove new one
by_hash.remove(&order.hash).expect("The hash has been just inserted and no other line is altering `by_hash`."); let order = by_hash.remove(&order.hash).expect("The hash has been just inserted and no other line is altering `by_hash`.");
if order.origin.is_local() {
local.mark_replaced(order.transaction, old_fee, old_hash);
}
false false
} else { } else {
trace!(target: "txqueue", "Replaced transaction: {:?} with transaction with higher gas price: {:?}", old.hash, order.hash); trace!(target: "txqueue", "Replaced transaction: {:?} with transaction with higher gas price: {:?}", old.hash, order.hash);
// Make sure we remove old transaction entirely // Make sure we remove old transaction entirely
by_hash.remove(&old.hash).expect("The hash is coming from `future` so it has to be in `by_hash`."); let old = by_hash.remove(&old.hash).expect("The hash is coming from `future` so it has to be in `by_hash`.");
if old.origin.is_local() {
local.mark_replaced(old.transaction, new_fee, new_hash);
}
true true
} }
} }
@ -1078,6 +1216,7 @@ mod test {
use error::{Error, TransactionError}; use error::{Error, TransactionError};
use super::*; use super::*;
use super::{TransactionSet, TransactionOrder, VerifiedTransaction}; use super::{TransactionSet, TransactionOrder, VerifiedTransaction};
use miner::local_transactions::LocalTransactionsList;
use client::TransactionImportResult; use client::TransactionImportResult;
fn unwrap_tx_err(err: Result<TransactionImportResult, Error>) -> TransactionError { fn unwrap_tx_err(err: Result<TransactionImportResult, Error>) -> TransactionError {
@ -1208,6 +1347,7 @@ mod test {
#[test] #[test]
fn should_create_transaction_set() { fn should_create_transaction_set() {
// given // given
let mut local = LocalTransactionsList::default();
let mut set = TransactionSet { let mut set = TransactionSet {
by_priority: BTreeSet::new(), by_priority: BTreeSet::new(),
by_address: Table::new(), by_address: Table::new(),
@ -1235,7 +1375,7 @@ mod test {
assert_eq!(set.by_address.len(), 2); assert_eq!(set.by_address.len(), 2);
// when // when
set.enforce_limit(&mut by_hash); set.enforce_limit(&mut by_hash, &mut local);
// then // then
assert_eq!(by_hash.len(), 1); assert_eq!(by_hash.len(), 1);
@ -1628,6 +1768,31 @@ mod test {
assert_eq!(top.len(), 2); assert_eq!(top.len(), 2);
} }
#[test]
fn when_importing_local_should_mark_others_from_the_same_sender_as_local() {
// given
let mut txq = TransactionQueue::default();
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
// the second one has same nonce but higher `gas_price`
let (_, tx0) = new_similar_tx_pair();
txq.add(tx0.clone(), TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap();
txq.add(tx1.clone(), TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap();
// the one with higher gas price is first
assert_eq!(txq.top_transactions()[0], tx0);
assert_eq!(txq.top_transactions()[1], tx1);
// when
// insert second as local
txq.add(tx2.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
// then
// the order should be updated
assert_eq!(txq.top_transactions()[0], tx1);
assert_eq!(txq.top_transactions()[1], tx2);
assert_eq!(txq.top_transactions()[2], tx0);
}
#[test] #[test]
fn should_prioritize_reimported_transactions_within_same_nonce_height() { fn should_prioritize_reimported_transactions_within_same_nonce_height() {
// given // given
@ -1695,6 +1860,38 @@ mod test {
assert_eq!(top.len(), 4); assert_eq!(top.len(), 4);
} }
#[test]
fn should_not_penalize_local_transactions() {
// given
let mut txq = TransactionQueue::default();
// txa, txb - slightly bigger gas price to have consistent ordering
let (txa, txb) = new_tx_pair_default(1.into(), 0.into());
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
// insert everything
txq.add(txa.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(txb.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(tx1.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(tx2.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
let top = txq.top_transactions();
assert_eq!(top[0], tx1);
assert_eq!(top[1], txa);
assert_eq!(top[2], tx2);
assert_eq!(top[3], txb);
assert_eq!(top.len(), 4);
// when
txq.penalize(&tx1.hash());
// then (order is the same)
let top = txq.top_transactions();
assert_eq!(top[0], tx1);
assert_eq!(top[1], txa);
assert_eq!(top[2], tx2);
assert_eq!(top[3], txb);
assert_eq!(top.len(), 4);
}
#[test] #[test]
fn should_penalize_transactions_from_sender() { fn should_penalize_transactions_from_sender() {
@ -1940,12 +2137,11 @@ mod test {
let mut txq = TransactionQueue::with_limits(PrioritizationStrategy::GasPriceOnly, 100, default_gas_val() * U256::from(2), !U256::zero()); let mut txq = TransactionQueue::with_limits(PrioritizationStrategy::GasPriceOnly, 100, default_gas_val() * U256::from(2), !U256::zero());
let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1)); let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1));
let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2)); let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2));
let (tx5, tx6) = new_tx_pair_default(U256::from(1), U256::from(2)); let (tx5, _) = new_tx_pair_default(U256::from(1), U256::from(2));
txq.add(tx1.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap(); txq.add(tx1.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(tx2.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap(); txq.add(tx2.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(tx5.clone(), TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap();
// Not accepted because of limit // Not accepted because of limit
txq.add(tx6.clone(), TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap_err(); txq.add(tx5.clone(), TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap_err();
txq.add(tx3.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap(); txq.add(tx3.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
txq.add(tx4.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap(); txq.add(tx4.clone(), TransactionOrigin::Local, &default_account_details, &gas_estimator).unwrap();
assert_eq!(txq.status().pending, 4); assert_eq!(txq.status().pending, 4);

View File

@ -86,7 +86,7 @@ impl SecretStore for EthStore {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> { fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed)); let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed));
let id: [u8; 16] = Random::random(); let id: [u8; 16] = Random::random();
let account = SafeAccount::create(&keypair, id, password, self.iterations, UUID::from(id).into(), "{}".to_owned()); let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned());
let address = account.address.clone(); let address = account.address.clone();
try!(self.save(account)); try!(self.save(account));
Ok(address) Ok(address)

View File

@ -16,7 +16,7 @@
//! Binary representation of types //! Binary representation of types
use util::{U256, U512, H256, H2048, Address}; use util::{U256, U512, H256, H512, H2048, Address};
use std::mem; use std::mem;
use std::collections::{VecDeque, BTreeMap}; use std::collections::{VecDeque, BTreeMap};
use std::ops::Range; use std::ops::Range;
@ -800,6 +800,7 @@ binary_fixed_size!(bool);
binary_fixed_size!(U256); binary_fixed_size!(U256);
binary_fixed_size!(U512); binary_fixed_size!(U512);
binary_fixed_size!(H256); binary_fixed_size!(H256);
binary_fixed_size!(H512);
binary_fixed_size!(H2048); binary_fixed_size!(H2048);
binary_fixed_size!(Address); binary_fixed_size!(Address);
binary_fixed_size!(BinHandshake); binary_fixed_size!(BinHandshake);

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.52", "version": "0.2.58",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -43,6 +43,7 @@
"test": "mocha 'src/**/*.spec.js'", "test": "mocha 'src/**/*.spec.js'",
"test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'", "test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'",
"test:e2e": "mocha 'src/**/*.e2e.js'", "test:e2e": "mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached" "prepush": "npm run lint:cached"
}, },
"devDependencies": { "devDependencies": {
@ -101,9 +102,10 @@
"postcss-nested": "^1.0.0", "postcss-nested": "^1.0.0",
"postcss-simple-vars": "^3.0.0", "postcss-simple-vars": "^3.0.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"react-addons-test-utils": "^15.3.0", "react-addons-test-utils": "~15.3.2",
"react-copy-to-clipboard": "^4.2.3", "react-copy-to-clipboard": "^4.2.3",
"react-hot-loader": "^1.3.0", "react-dom": "~15.3.2",
"react-hot-loader": "~1.3.0",
"rucksack-css": "^0.8.6", "rucksack-css": "^0.8.6",
"sinon": "^1.17.4", "sinon": "^1.17.4",
"sinon-as-promised": "^4.0.2", "sinon-as-promised": "^4.0.2",
@ -113,7 +115,7 @@
"webpack": "^1.13.2", "webpack": "^1.13.2",
"webpack-dev-server": "^1.15.2", "webpack-dev-server": "^1.15.2",
"webpack-error-notification": "0.1.6", "webpack-error-notification": "0.1.6",
"webpack-hot-middleware": "^2.7.1", "webpack-hot-middleware": "~2.13.2",
"websocket": "^1.0.23" "websocket": "^1.0.23"
}, },
"dependencies": { "dependencies": {
@ -133,7 +135,7 @@
"js-sha3": "^0.5.2", "js-sha3": "^0.5.2",
"lodash": "^4.11.1", "lodash": "^4.11.1",
"marked": "^0.3.6", "marked": "^0.3.6",
"material-ui": "^0.16.1", "material-ui": "0.16.1",
"material-ui-chip-input": "^0.8.0", "material-ui-chip-input": "^0.8.0",
"mobx": "^2.6.1", "mobx": "^2.6.1",
"mobx-react": "^3.5.8", "mobx-react": "^3.5.8",
@ -141,16 +143,16 @@
"moment": "^2.14.1", "moment": "^2.14.1",
"phoneformat.js": "^1.0.3", "phoneformat.js": "^1.0.3",
"qs": "^6.3.0", "qs": "^6.3.0",
"react": "^15.2.1", "react": "~15.3.2",
"react-ace": "^4.0.0", "react-ace": "^4.0.0",
"react-addons-css-transition-group": "^15.2.1", "react-addons-css-transition-group": "~15.3.2",
"react-chartjs-2": "^1.5.0", "react-chartjs-2": "^1.5.0",
"react-dom": "^15.2.1", "react-dom": "~15.3.2",
"react-dropzone": "^3.7.3", "react-dropzone": "^3.7.3",
"react-redux": "^4.4.5", "react-redux": "^4.4.5",
"react-router": "^2.6.1", "react-router": "^2.6.1",
"react-router-redux": "^4.0.5", "react-router-redux": "^4.0.5",
"react-tap-event-plugin": "^1.0.0", "react-tap-event-plugin": "~1.0.0",
"react-tooltip": "^2.0.3", "react-tooltip": "^2.0.3",
"recharts": "^0.15.2", "recharts": "^0.15.2",
"redux": "^3.5.2", "redux": "^3.5.2",

View File

@ -27,6 +27,7 @@
}, },
"dependencies": { "dependencies": {
"bignumber.js": "^2.3.0", "bignumber.js": "^2.3.0",
"js-sha3": "^0.5.2" "js-sha3": "^0.5.2",
"node-fetch": "^1.6.3"
} }
} }

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inData, inHex, inNumber16, inOptions } from '../../format/input'; import { inAddress, inData, inHex, inNumber16, inOptions } from '../../format/input';
import { outAccountInfo, outAddress, outHistogram, outNumber, outPeers } from '../../format/output'; import { outAccountInfo, outAddress, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
export default class Parity { export default class Parity {
constructor (transport) { constructor (transport) {
@ -117,16 +117,29 @@ export default class Parity {
.execute('parity_hashContent', url); .execute('parity_hashContent', url);
} }
importGethAccounts (accounts) {
return this._transport
.execute('parity_importGethAccounts', (accounts || []).map(inAddress))
.then((accounts) => (accounts || []).map(outAddress));
}
listGethAccounts () { listGethAccounts () {
return this._transport return this._transport
.execute('parity_listGethAccounts') .execute('parity_listGethAccounts')
.then((accounts) => (accounts || []).map(outAddress)); .then((accounts) => (accounts || []).map(outAddress));
} }
importGethAccounts (accounts) { localTransactions () {
return this._transport return this._transport
.execute('parity_importGethAccounts', (accounts || []).map(inAddress)) .execute('parity_localTransactions')
.then((accounts) => (accounts || []).map(outAddress)); .then(transactions => {
Object.values(transactions)
.filter(tx => tx.transaction)
.map(tx => {
tx.transaction = outTransaction(tx.transaction);
});
return transactions;
});
} }
minGasPrice () { minGasPrice () {
@ -192,6 +205,17 @@ export default class Parity {
.execute('parity_nodeName'); .execute('parity_nodeName');
} }
pendingTransactions () {
return this._transport
.execute('parity_pendingTransactions')
.then(data => data.map(outTransaction));
}
pendingTransactionsStats () {
return this._transport
.execute('parity_pendingTransactionsStats');
}
phraseToAddress (phrase) { phraseToAddress (phrase) {
return this._transport return this._transport
.execute('parity_phraseToAddress', phrase) .execute('parity_phraseToAddress', phrase)

View File

@ -84,7 +84,7 @@ export default class Ws extends JsonRpcBase {
this._connecting = false; this._connecting = false;
if (this._autoConnect) { if (this._autoConnect) {
this._connect(); setTimeout(() => this._connect(), 500);
} }
} }

View File

@ -15,12 +15,17 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.container { .body {
text-align: center;
background: #333; background: #333;
color: #fff;
}
.container {
font-family: 'Roboto'; font-family: 'Roboto';
vertical-align: middle; vertical-align: middle;
padding: 4em 0; padding: 4em 0;
text-align: center; margin: 0 0 2em 0;
} }
.form { .form {
@ -98,7 +103,7 @@
color: #333; color: #333;
background: #eee; background: #eee;
border: none; border: none;
border-radius: 5px; border-radius: 0.5em;
width: 100%; width: 100%;
font-size: 1em; font-size: 1em;
text-align: center; text-align: center;
@ -113,20 +118,29 @@
} }
.hashError, .hashWarning, .hashOk { .hashError, .hashWarning, .hashOk {
padding-top: 0.5em; margin: 0.5em 0;
text-align: center; text-align: center;
padding: 1em 0;
border: 0.25em solid #333;
border-radius: 0.5em;
} }
.hashError { .hashError {
border-color: #f66;
color: #f66; color: #f66;
background: rgba(255, 102, 102, 0.25);
} }
.hashWarning { .hashWarning {
border-color: #f80;
color: #f80; color: #f80;
background: rgba(255, 236, 0, 0.25);
} }
.hashOk { .hashOk {
opacity: 0.5; border-color: #6f6;
color: #6f6;
background: rgba(102, 255, 102, 0.25);
} }
.typeButtons { .typeButtons {

View File

@ -19,6 +19,7 @@ import React, { Component } from 'react';
import { api } from '../parity'; import { api } from '../parity';
import { attachInterface } from '../services'; import { attachInterface } from '../services';
import Button from '../Button'; import Button from '../Button';
import Events from '../Events';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import Loading from '../Loading'; import Loading from '../Loading';
@ -27,6 +28,8 @@ import styles from './application.css';
const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
let nextEventId = 0;
export default class Application extends Component { export default class Application extends Component {
state = { state = {
fromAddress: null, fromAddress: null,
@ -43,7 +46,9 @@ export default class Application extends Component {
registerState: '', registerState: '',
registerType: 'file', registerType: 'file',
repo: '', repo: '',
repoError: null repoError: null,
events: {},
eventIds: []
} }
componentDidMount () { componentDidMount () {
@ -75,7 +80,7 @@ export default class Application extends Component {
let hashClass = null; let hashClass = null;
if (contentHashError) { if (contentHashError) {
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning; hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
} else { } else if (contentHash) {
hashClass = styles.hashOk; hashClass = styles.hashOk;
} }
@ -116,6 +121,7 @@ export default class Application extends Component {
} }
return ( return (
<div className={ styles.body }>
<div className={ styles.container }> <div className={ styles.container }>
<div className={ styles.form }> <div className={ styles.form }>
<div className={ styles.typeButtons }> <div className={ styles.typeButtons }>
@ -140,6 +146,10 @@ export default class Application extends Component {
</div> </div>
</div> </div>
</div> </div>
<Events
eventIds={ this.state.eventIds }
events={ this.state.events } />
</div>
); );
} }
@ -285,15 +295,29 @@ export default class Application extends Component {
} }
} }
trackRequest (promise) { trackRequest (eventId, promise) {
return promise return promise
.then((signerRequestId) => { .then((signerRequestId) => {
this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
signerRequestId,
registerState: 'Transaction posted, Waiting for transaction authorization'
})
})
});
return api.pollMethod('parity_checkRequest', signerRequestId); return api.pollMethod('parity_checkRequest', signerRequestId);
}) })
.then((txHash) => { .then((txHash) => {
this.setState({ txHash, registerState: 'Transaction authorized, Waiting for network confirmations' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
txHash,
registerState: 'Transaction authorized, Waiting for network confirmations'
})
})
});
return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => { return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => {
if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) { if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) {
@ -304,27 +328,72 @@ export default class Application extends Component {
}); });
}) })
.then((txReceipt) => { .then((txReceipt) => {
this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', commit: '', repo: '', commitError: null, contentHash: '', contentHashOwner: null, contentHashError: null }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
txReceipt,
registerBusy: false,
registerState: 'Network confirmed, Received transaction receipt'
})
})
});
}) })
.catch((error) => { .catch((error) => {
console.error('onSend', error); console.error('onSend', error);
this.setState({ registerError: error.message });
this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: error.message,
registerError: true,
registerBusy: false
})
})
});
}); });
} }
registerContent (repo, commit) { registerContent (contentRepo, contentCommit) {
const { contentHash, fromAddress, instance } = this.state; const { contentHash, fromAddress, instance } = this.state;
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); const eventId = nextEventId++;
const values = [contentHash, contentRepo, contentCommit];
const values = [contentHash, repo, commit.substr(0, 2) === '0x' ? commit : `0x${commit}`];
const options = { from: fromAddress }; const options = { from: fromAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
events: Object.assign({}, this.state.events, {
[eventId]: {
contentHash,
contentRepo,
contentCommit,
fromAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
}
}),
url: '',
commit: '',
repo: '',
commitError: null,
contentHash: '',
contentHashOwner: null,
contentHashError: null
});
this.trackRequest( this.trackRequest(
instance eventId, instance
.hint.estimateGas(options, values) .hint.estimateGas(options, values)
.then((gas) => { .then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: 'Gas estimated, Posting transaction to the network'
})
})
});
const gasPassed = gas.mul(1.2); const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0); options.gas = gasPassed.toFixed(0);
@ -335,19 +404,45 @@ export default class Application extends Component {
); );
} }
registerUrl (url) { registerUrl (contentUrl) {
const { contentHash, fromAddress, instance } = this.state; const { contentHash, fromAddress, instance } = this.state;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); const eventId = nextEventId++;
const values = [contentHash, contentUrl];
const values = [contentHash, url];
const options = { from: fromAddress }; const options = { from: fromAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
events: Object.assign({}, this.state.events, {
[eventId]: {
contentHash,
contentUrl,
fromAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
}
}),
url: '',
commit: '',
repo: '',
commitError: null,
contentHash: '',
contentHashOwner: null,
contentHashError: null
});
this.trackRequest( this.trackRequest(
instance eventId, instance
.hintURL.estimateGas(options, values) .hintURL.estimateGas(options, values)
.then((gas) => { .then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: 'Gas estimated, Posting transaction to the network'
})
})
});
const gasPassed = gas.mul(1.2); const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0); options.gas = gasPassed.toFixed(0);

View File

@ -0,0 +1,37 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.list {
border: none;
margin: 0 auto;
text-align: left;
vertical-align: top;
tr {
&[data-busy="true"] {
opacity: 0.5;
}
&[data-error="true"] {
color: #f66;
}
}
td {
padding: 0.5em;
}
}

View File

@ -0,0 +1,52 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import moment from 'moment';
import styles from './events.css';
export default class Events extends Component {
static propTypes = {
eventIds: PropTypes.array.isRequired,
events: PropTypes.array.isRequired
}
render () {
return (
<table className={ styles.list }>
<tbody>
{ this.props.eventIds.map((id) => this.renderEvent(id, this.props.events[id])) }
</tbody>
</table>
);
}
renderEvent = (eventId, event) => {
return (
<tr key={ `event_${eventId}` } data-busy={ event.registerBusy } data-error={ event.registerError }>
<td>
<div>{ moment(event.timestamp).fromNow() }</div>
<div>{ event.registerState }</div>
</td>
<td>
<div>{ event.contentUrl || `${event.contentRepo}/${event.contentCommit}` }</div>
<div>{ event.contentHash }</div>
</td>
</tr>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './events';

17
js/src/dapps/localtx.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="/parity-logo-black-no-text.png" type="image/png">
<title>Local transactions Viewer</title>
</head>
<body>
<div id="container"></div>
<script src="vendor.js"></script>
<script src="commons.js"></script>
<script src="/parity-utils/parity.js"></script>
<script src="localtx.js"></script>
</body>
</html>

33
js/src/dapps/localtx.js Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import ReactDOM from 'react-dom';
import React from 'react';
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
import Application from './localtx/Application';
import '../../assets/fonts/Roboto/font.css';
import '../../assets/fonts/RobotoMono/font.css';
import './style.css';
import './localtx.html';
ReactDOM.render(
<Application />,
document.querySelector('#container')
);

View File

@ -0,0 +1,19 @@
.container {
padding: 1rem 2rem;
text-align: center;
h1 {
margin-top: 3rem;
margin-bottom: 1rem;
}
table {
text-align: left;
margin: auto;
max-width: 90vw;
th {
text-align: center;
}
}
}

View File

@ -0,0 +1,203 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component } from 'react';
import { api } from '../parity';
import styles from './application.css';
import { Transaction, LocalTransaction } from '../Transaction';
export default class Application extends Component {
state = {
loading: true,
transactions: [],
localTransactions: {},
blockNumber: 0
}
componentDidMount () {
const poll = () => this.fetchTransactionData().then(poll).catch(poll);
this._timeout = setTimeout(poll, 2000);
}
componentWillUnmount () {
clearTimeout(this._timeout);
}
fetchTransactionData () {
return Promise.all([
api.parity.pendingTransactions(),
api.parity.pendingTransactionsStats(),
api.parity.localTransactions(),
api.eth.blockNumber()
]).then(([pending, stats, local, blockNumber]) => {
// Combine results together
const transactions = pending.map(tx => {
return {
transaction: tx,
stats: stats[tx.hash],
isLocal: !!local[tx.hash]
};
});
// Add transaction data to locals
transactions
.filter(tx => tx.isLocal)
.map(data => {
const tx = data.transaction;
local[tx.hash].transaction = tx;
local[tx.hash].stats = data.stats;
});
// Convert local transactions to array
const localTransactions = Object.keys(local).map(hash => {
const data = local[hash];
data.txHash = hash;
return data;
});
// Sort local transactions by nonce (move future to the end)
localTransactions.sort((a, b) => {
a = a.transaction || {};
b = b.transaction || {};
if (a.from && b.from && a.from !== b.from) {
return a.from < b.from;
}
if (!a.nonce || !b.nonce) {
return !a.nonce ? 1 : -1;
}
return new BigNumber(a.nonce).comparedTo(new BigNumber(b.nonce));
});
this.setState({
loading: false,
transactions,
localTransactions,
blockNumber
});
});
}
render () {
const { loading } = this.state;
if (loading) {
return (
<div className={ styles.container }>Loading...</div>
);
}
return (
<div className={ styles.container }>
<h1>Your local transactions</h1>
{ this.renderLocals() }
<h1>Transactions in the queue</h1>
{ this.renderQueueSummary() }
{ this.renderQueue() }
</div>
);
}
renderQueueSummary () {
const { transactions } = this.state;
if (!transactions.length) {
return null;
}
const count = transactions.length;
const locals = transactions.filter(tx => tx.isLocal).length;
const fee = transactions
.map(tx => tx.transaction)
.map(tx => tx.gasPrice.mul(tx.gas))
.reduce((sum, fee) => sum.add(fee), new BigNumber(0));
return (
<h3>
Count: <strong>{ locals ? `${count} (${locals})` : count }</strong>
&nbsp;
Total Fee: <strong>{ api.util.fromWei(fee).toFixed(3) } ETH</strong>
</h3>
);
}
renderQueue () {
const { blockNumber, transactions } = this.state;
if (!transactions.length) {
return (
<h3>The queue seems is empty.</h3>
);
}
return (
<table cellSpacing='0'>
<thead>
{ Transaction.renderHeader() }
</thead>
<tbody>
{
transactions.map((tx, idx) => (
<Transaction
key={ tx.transaction.hash }
idx={ idx + 1 }
isLocal={ tx.isLocal }
transaction={ tx.transaction }
stats={ tx.stats }
blockNumber={ blockNumber }
/>
))
}
</tbody>
</table>
);
}
renderLocals () {
const { localTransactions } = this.state;
if (!localTransactions.length) {
return (
<h3>You haven't sent any transactions yet.</h3>
);
}
return (
<table cellSpacing='0'>
<thead>
{ LocalTransaction.renderHeader() }
</thead>
<tbody>
{
localTransactions.map(tx => (
<LocalTransaction
key={ tx.txHash }
hash={ tx.txHash }
transaction={ tx.transaction }
status={ tx.status }
stats={ tx.stats }
details={ tx }
/>
))
}
</tbody>
</table>
);
}
}

View File

@ -0,0 +1,32 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import '../../../environment/tests';
import Application from './application';
describe('localtx/Application', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(<Application />);
expect(rendered).to.be.defined;
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './application';

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export { Transaction, LocalTransaction } from './transaction';

View File

@ -0,0 +1,31 @@
.from {
white-space: nowrap;
img {
vertical-align: middle;
}
}
.transaction {
td {
padding: 7px 15px;
}
td:first-child {
padding: 7px 0;
}
&.local {
background: #8bc34a;
}
}
.nowrap {
white-space: nowrap;
}
.edit {
label, input {
display: block;
}
}

View File

@ -0,0 +1,382 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import { api } from '../parity';
import styles from './transaction.css';
import IdentityIcon from '../../githubhint/IdentityIcon';
class BaseTransaction extends Component {
shortHash (hash) {
return `${hash.substr(0, 5)}..${hash.substr(hash.length - 3)}`;
}
renderHash (hash) {
return (
<code title={ hash }>
{ this.shortHash(hash) }
</code>
);
}
renderFrom (transaction) {
if (!transaction) {
return '-';
}
return (
<div title={ transaction.from } className={ styles.from }>
<IdentityIcon
address={ transaction.from }
/>
0x{ transaction.nonce.toString(16) }
</div>
);
}
renderGasPrice (transaction) {
if (!transaction) {
return '-';
}
return (
<span title={ `${transaction.gasPrice.toFormat(0)} wei` }>
{ api.util.fromWei(transaction.gasPrice, 'shannon').toFormat(2) }&nbsp;shannon
</span>
);
}
renderGas (transaction) {
if (!transaction) {
return '-';
}
return (
<span title={ `${transaction.gas.toFormat(0)} Gas` }>
{ transaction.gas.div(10 ** 6).toFormat(3) }&nbsp;MGas
</span>
);
}
renderPropagation (stats) {
const noOfPeers = Object.keys(stats.propagatedTo).length;
const noOfPropagations = Object.values(stats.propagatedTo).reduce((sum, val) => sum + val, 0);
return (
<span className={ styles.nowrap }>
{ noOfPropagations } ({ noOfPeers } peers)
</span>
);
}
}
export class Transaction extends BaseTransaction {
static propTypes = {
idx: PropTypes.number.isRequired,
transaction: PropTypes.object.isRequired,
blockNumber: PropTypes.object.isRequired,
isLocal: PropTypes.bool,
stats: PropTypes.object
};
static defaultProps = {
isLocal: false,
stats: {
firstSeen: 0,
propagatedTo: {}
}
};
static renderHeader () {
return (
<tr className={ styles.header }>
<th></th>
<th>
Transaction
</th>
<th>
From
</th>
<th>
Gas Price
</th>
<th>
Gas
</th>
<th>
First propagation
</th>
<th>
# Propagated
</th>
<th>
</th>
</tr>
);
}
render () {
const { isLocal, stats, transaction, idx } = this.props;
const blockNo = new BigNumber(stats.firstSeen);
const clazz = classnames(styles.transaction, {
[styles.local]: isLocal
});
return (
<tr className={ clazz }>
<td>
{ idx }.
</td>
<td>
{ this.renderHash(transaction.hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td>
{ this.renderGasPrice(transaction) }
</td>
<td>
{ this.renderGas(transaction) }
</td>
<td title={ blockNo.toFormat(0) }>
{ this.renderTime(stats.firstSeen) }
</td>
<td>
{ this.renderPropagation(stats) }
</td>
</tr>
);
}
renderTime (firstSeen) {
const { blockNumber } = this.props;
if (!firstSeen) {
return 'never';
}
const timeInMinutes = blockNumber.sub(firstSeen).mul(14).div(60).toFormat(1);
return `${timeInMinutes} minutes ago`;
}
}
export class LocalTransaction extends BaseTransaction {
static propTypes = {
hash: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
transaction: PropTypes.object,
isLocal: PropTypes.bool,
stats: PropTypes.object,
details: PropTypes.object
};
static defaultProps = {
stats: {
propagatedTo: {}
}
};
static renderHeader () {
return (
<tr className={ styles.header }>
<th></th>
<th>
Transaction
</th>
<th>
From
</th>
<th>
Gas Price / Gas
</th>
<th>
Status
</th>
</tr>
);
}
state = {
isSending: false,
isResubmitting: false,
gasPrice: null,
gas: null
};
toggleResubmit = () => {
const { transaction } = this.props;
const { isResubmitting, gasPrice } = this.state;
this.setState({
isResubmitting: !isResubmitting
});
if (gasPrice === null) {
this.setState({
gasPrice: `0x${transaction.gasPrice.toString(16)}`,
gas: `0x${transaction.gas.toString(16)}`
});
}
};
setGasPrice = el => {
this.setState({
gasPrice: el.target.value
});
};
setGas = el => {
this.setState({
gas: el.target.value
});
};
sendTransaction = () => {
const { transaction } = this.props;
const { gasPrice, gas } = this.state;
const newTransaction = {
from: transaction.from,
to: transaction.to,
nonce: transaction.nonce,
value: transaction.value,
data: transaction.data,
gasPrice, gas
};
this.setState({
isResubmitting: false,
isSending: true
});
const closeSending = () => this.setState({
isSending: false,
gasPrice: null,
gas: null
});
api.eth.sendTransaction(newTransaction)
.then(closeSending)
.catch(closeSending);
};
render () {
if (this.state.isResubmitting) {
return this.renderResubmit();
}
const { stats, transaction, hash, status } = this.props;
const { isSending } = this.state;
const resubmit = isSending ? (
'sending...'
) : (
<a href='javascript:void' onClick={ this.toggleResubmit }>
resubmit
</a>
);
return (
<tr className={ styles.transaction }>
<td>
{ !transaction ? null : resubmit }
</td>
<td>
{ this.renderHash(hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td>
{ this.renderGasPrice(transaction) }
<br />
{ this.renderGas(transaction) }
</td>
<td>
{ this.renderStatus() }
<br />
{ status === 'pending' ? this.renderPropagation(stats) : null }
</td>
</tr>
);
}
renderStatus () {
const { details } = this.props;
let state = {
'pending': () => 'In queue: Pending',
'future': () => 'In queue: Future',
'mined': () => 'Mined',
'dropped': () => 'Dropped because of queue limit',
'invalid': () => 'Transaction is invalid',
'rejected': () => `Rejected: ${details.error}`,
'replaced': () => `Replaced by ${this.shortHash(details.hash)}`
}[this.props.status];
return state ? state() : 'unknown';
}
// TODO [ToDr] Gas Price / Gas selection is not needed
// when signer supports gasPrice/gas tunning.
renderResubmit () {
const { transaction } = this.props;
const { gasPrice, gas } = this.state;
return (
<tr className={ styles.transaction }>
<td>
<a href='javascript:void' onClick={ this.toggleResubmit }>
cancel
</a>
</td>
<td>
{ this.renderHash(transaction.hash) }
</td>
<td>
{ this.renderFrom(transaction) }
</td>
<td className={ styles.edit }>
<input
type='text'
value={ gasPrice }
onChange={ this.setGasPrice }
/>
<input
type='text'
value={ gas }
onChange={ this.setGas }
/>
</td>
<td colSpan='2'>
<a href='javascript:void' onClick={ this.sendTransaction }>
Send
</a>
</td>
</tr>
);
}
}

View File

@ -0,0 +1,67 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import '../../../environment/tests';
import EthApi from '../../../api';
// Mock API for tests
import * as Api from '../parity';
Api.api = {
util: EthApi.prototype.util
};
import BigNumber from 'bignumber.js';
import { Transaction, LocalTransaction } from './transaction';
describe('localtx/Transaction', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const transaction = {
hash: '0x1234567890',
nonce: 15,
gasPrice: new BigNumber(10),
gas: new BigNumber(10)
};
const rendered = shallow(
<Transaction
isLocal={ false }
transaction={ transaction }
blockNumber={ new BigNumber(0) }
/>
);
expect(rendered).to.be.defined;
});
});
});
describe('localtx/LocalTransaction', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(
<LocalTransaction
hash={ '0x1234567890' }
status={ 'pending' }
/>
);
expect(rendered).to.be.defined;
});
});
});

View File

@ -0,0 +1,21 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
const api = window.parent.secureApi;
export {
api
};

View File

@ -224,15 +224,6 @@ export default {
} }
}, },
listGethAccounts: {
desc: 'Returns a list of the accounts available from Geth',
params: [],
returns: {
type: Array,
desc: '20 Bytes addresses owned by the client.'
}
},
importGethAccounts: { importGethAccounts: {
desc: 'Imports a list of accounts from geth', desc: 'Imports a list of accounts from geth',
params: [ params: [
@ -247,6 +238,24 @@ export default {
} }
}, },
listGethAccounts: {
desc: 'Returns a list of the accounts available from Geth',
params: [],
returns: {
type: Array,
desc: '20 Bytes addresses owned by the client.'
}
},
localTransactions: {
desc: 'Returns an object of current and past local transactions.',
params: [],
returns: {
type: Object,
desc: 'Mapping of `tx hash` into status object.'
}
},
minGasPrice: { minGasPrice: {
desc: 'Returns currently set minimal gas price', desc: 'Returns currently set minimal gas price',
params: [], params: [],
@ -379,6 +388,24 @@ export default {
} }
}, },
pendingTransactions: {
desc: 'Returns a list of transactions currently in the queue.',
params: [],
returns: {
type: Array,
desc: 'Transactions ordered by priority'
}
},
pendingTransactionsStats: {
desc: 'Returns propagation stats for transactions in the queue',
params: [],
returns: {
type: Object,
desc: 'mapping of `tx hash` into `stats`'
}
},
phraseToAddress: { phraseToAddress: {
desc: 'Converts a secret phrase into the corresponting address', desc: 'Converts a secret phrase into the corresponting address',
params: [ params: [

View File

@ -14,10 +14,22 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import 'babel-polyfill/dist/polyfill.js';
import es6Promise from 'es6-promise';
es6Promise.polyfill();
const isNode = typeof global !== 'undefined' && typeof global !== 'undefined';
const isBrowser = typeof self !== 'undefined' && typeof self.window !== 'undefined';
if (isBrowser) {
require('whatwg-fetch');
}
if (isNode) {
global.fetch = require('node-fetch');
}
import Abi from './abi'; import Abi from './abi';
import Api from './api'; import Api from './api';
export { module.exports = { Api, Abi };
Abi,
Api
};

View File

@ -14,19 +14,3 @@
/* You should have received a copy of the GNU General Public License /* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.spaced {
margin: 0.25em 0;
}
.typeContainer {
display: flex;
flex-direction: column;
.desc {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
}
}

View File

@ -20,13 +20,10 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '../../ui';
import { Button, Modal, Form, Input, InputAddress } from '../../ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation';
import { eip20, wallet } from '../../contracts/abi'; import { eip20, wallet } from '../../contracts/abi';
import styles from './addContract.css';
const ABI_TYPES = [ const ABI_TYPES = [
{ {
@ -105,13 +102,12 @@ export default class AddContract extends Component {
const { abiTypeIndex } = this.state; const { abiTypeIndex } = this.state;
return ( return (
<RadioButtonGroup <RadioButtons
valueSelected={ abiTypeIndex }
name='contractType' name='contractType'
value={ abiTypeIndex }
values={ this.getAbiTypes() }
onChange={ this.onChangeABIType } onChange={ this.onChangeABIType }
> />
{ this.renderAbiTypes() }
</RadioButtonGroup>
); );
} }
@ -194,20 +190,13 @@ export default class AddContract extends Component {
); );
} }
renderAbiTypes () { getAbiTypes () {
return ABI_TYPES.map((type, index) => ( return ABI_TYPES.map((type, index) => ({
<RadioButton label: type.label,
className={ styles.spaced } description: type.description,
value={ index } key: index,
label={ ( ...type
<div className={ styles.typeContainer }> }));
<span>{ type.label }</span>
<span className={ styles.desc }>{ type.description }</span>
</div>
) }
key={ index }
/>
));
} }
onNext = () => { onNext = () => {
@ -218,8 +207,8 @@ export default class AddContract extends Component {
this.setState({ step: this.state.step - 1 }); this.setState({ step: this.state.step - 1 });
} }
onChangeABIType = (event, index) => { onChangeABIType = (value, index) => {
const abiType = ABI_TYPES[index]; const abiType = value || ABI_TYPES[index];
this.setState({ abiTypeIndex: index, abiType }); this.setState({ abiTypeIndex: index, abiType });
this.onEditAbi(abiType.value); this.onEditAbi(abiType.value);
} }

View File

@ -15,13 +15,12 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, TypedInput } from '../../../ui'; import { AddressSelect, Form, Input, Select } from '../../../ui';
import { validateAbi } from '../../../util/validation'; import { validateAbi } from '../../../util/validation';
import { parseAbiType } from '../../../util/abi'; import { parseAbiType } from '../../../util/abi';
import styles from '../deployContract.css';
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -29,24 +28,26 @@ export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
abi: PropTypes.string,
abiError: PropTypes.string, onFromAddressChange: PropTypes.func.isRequired,
code: PropTypes.string, onNameChange: PropTypes.func.isRequired,
codeError: PropTypes.string, onDescriptionChange: PropTypes.func.isRequired,
description: PropTypes.string, onAbiChange: PropTypes.func.isRequired,
descriptionError: PropTypes.string, onCodeChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired,
onInputsChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
nameError: PropTypes.string, nameError: PropTypes.string,
params: PropTypes.array, description: PropTypes.string,
paramsError: PropTypes.array, descriptionError: PropTypes.string,
onAbiChange: PropTypes.func.isRequired, abi: PropTypes.string,
onCodeChange: PropTypes.func.isRequired, abiError: PropTypes.string,
onFromAddressChange: PropTypes.func.isRequired, code: PropTypes.string,
onDescriptionChange: PropTypes.func.isRequired, codeError: PropTypes.string,
onNameChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired,
readOnly: PropTypes.bool readOnly: PropTypes.bool
}; };
@ -55,7 +56,9 @@ export default class DetailsStep extends Component {
}; };
state = { state = {
inputs: [] solcOutput: '',
contracts: {},
selectedContractIndex: 0
} }
componentDidMount () { componentDidMount () {
@ -63,6 +66,7 @@ export default class DetailsStep extends Component {
if (abi) { if (abi) {
this.onAbiChange(abi); this.onAbiChange(abi);
this.setState({ solcOutput: abi });
} }
if (code) { if (code) {
@ -71,8 +75,19 @@ export default class DetailsStep extends Component {
} }
render () { render () {
const { accounts } = this.props; const {
const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError, readOnly } = this.props; accounts,
readOnly,
fromAddress, fromAddressError,
name, nameError,
description, descriptionError,
abiError,
code, codeError
} = this.props;
const { solcOutput, contracts } = this.state;
const solc = contracts && Object.keys(contracts).length > 0;
return ( return (
<Form> <Form>
@ -83,18 +98,30 @@ export default class DetailsStep extends Component {
error={ fromAddressError } error={ fromAddressError }
accounts={ accounts } accounts={ accounts }
onChange={ this.onFromAddressChange } /> onChange={ this.onFromAddressChange } />
<Input <Input
label='contract name' label='contract name'
hint='a name for the deployed contract' hint='a name for the deployed contract'
error={ nameError } error={ nameError }
value={ name } value={ name || '' }
onSubmit={ this.onNameChange } /> onChange={ this.onNameChange } />
<Input <Input
label='abi' label='contract description (optional)'
hint='the abi of the contract to deploy' hint='a description for the contract'
error={ descriptionError }
value={ description }
onChange={ this.onDescriptionChange } />
{ this.renderContractSelect() }
<Input
label='abi / solc combined-output'
hint='the abi of the contract to deploy or solc combined-output'
error={ abiError } error={ abiError }
value={ abi } value={ solcOutput }
onSubmit={ this.onAbiChange } onChange={ this.onSolcChange }
onSubmit={ this.onSolcSubmit }
readOnly={ readOnly } /> readOnly={ readOnly } />
<Input <Input
label='code' label='code'
@ -102,66 +129,108 @@ export default class DetailsStep extends Component {
error={ codeError } error={ codeError }
value={ code } value={ code }
onSubmit={ this.onCodeChange } onSubmit={ this.onCodeChange }
readOnly={ readOnly } /> readOnly={ readOnly || solc } />
{ this.renderConstructorInputs() }
</Form> </Form>
); );
} }
renderConstructorInputs () { renderContractSelect () {
const { accounts, params, paramsError } = this.props; const { contracts } = this.state;
const { inputs } = this.state;
if (!inputs || !inputs.length) { if (!contracts || Object.keys(contracts).length === 0) {
return null; return null;
} }
return inputs.map((input, index) => { const { selectedContractIndex } = this.state;
const onChange = (value) => this.onParamChange(index, value); const contractsItems = Object.keys(contracts).map((name, index) => (
<MenuItem
const label = `${input.name ? `${input.name}: ` : ''}${input.type}`; key={ index }
const value = params[index]; label={ name }
const error = paramsError[index]; value={ index }
const param = parseAbiType(input.type); >
{ name }
</MenuItem>
));
return ( return (
<div key={ index } className={ styles.funcparams }> <Select
<TypedInput label='select a contract'
label={ label } onChange={ this.onContractChange }
value={ value } value={ selectedContractIndex }
error={ error } >
accounts={ accounts } { contractsItems }
onChange={ onChange } </Select>
param={ param }
/>
</div>
); );
}
onContractChange = (event, index) => {
const { contracts } = this.state;
const contractName = Object.keys(contracts)[index];
const contract = contracts[contractName];
if (!this.props.name || this.props.name.trim() === '') {
this.onNameChange(null, contractName);
}
const { abi, bin } = contract;
const code = /^0x/.test(bin) ? bin : `0x${bin}`;
this.setState({ selectedContractIndex: index }, () => {
this.onAbiChange(abi);
this.onCodeChange(code);
}); });
} }
onSolcChange = (event, value) => {
// Change triggered only if valid
if (this.props.abiError) {
return null;
}
this.onSolcSubmit(value);
}
onSolcSubmit = (value) => {
try {
const solcParsed = JSON.parse(value);
if (!solcParsed || !solcParsed.contracts) {
throw new Error('Wrong solc output');
}
this.setState({ contracts: solcParsed.contracts }, () => {
this.onContractChange(null, 0);
});
} catch (e) {
this.setState({ contracts: null });
this.onAbiChange(value);
}
this.setState({ solcOutput: value });
}
onFromAddressChange = (event, fromAddress) => { onFromAddressChange = (event, fromAddress) => {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
onFromAddressChange(fromAddress); onFromAddressChange(fromAddress);
} }
onNameChange = (name) => { onNameChange = (event, name) => {
const { onNameChange } = this.props; const { onNameChange } = this.props;
onNameChange(name); onNameChange(name);
} }
onParamChange = (index, value) => { onDescriptionChange = (event, description) => {
const { params, onParamsChange } = this.props; const { onDescriptionChange } = this.props;
params[index] = value; onDescriptionChange(description);
onParamsChange(params);
} }
onAbiChange = (abi) => { onAbiChange = (abi) => {
const { api } = this.context; const { api } = this.context;
const { onAbiChange, onParamsChange } = this.props; const { onAbiChange, onParamsChange, onInputsChange } = this.props;
const { abiError, abiParsed } = validateAbi(abi, api); const { abiError, abiParsed } = validateAbi(abi, api);
if (!abiError) { if (!abiError) {
@ -176,10 +245,10 @@ export default class DetailsStep extends Component {
}); });
onParamsChange(params); onParamsChange(params);
this.setState({ inputs }); onInputsChange(inputs);
} else { } else {
onParamsChange([]); onParamsChange([]);
this.setState({ inputs: [] }); onInputsChange([]);
} }
onAbiChange(abi); onAbiChange(abi);

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './parametersStep';

View File

@ -0,0 +1,105 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Form, TypedInput } from '../../../ui';
import { parseAbiType } from '../../../util/abi';
import styles from '../deployContract.css';
export default class ParametersStep extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
onParamsChange: PropTypes.func.isRequired,
inputs: PropTypes.array,
params: PropTypes.array,
paramsError: PropTypes.array
};
render () {
return (
<Form>
{ this.renderConstructorInputs() }
</Form>
);
}
renderConstructorInputs () {
const { accounts, params, paramsError } = this.props;
const { inputs } = this.props;
if (!inputs || !inputs.length) {
return null;
}
const inputsComponents = inputs.map((input, index) => {
const onChange = (value) => this.onParamChange(index, value);
const label = `${input.name ? `${input.name}: ` : ''}${input.type}`;
const value = params[index];
const error = paramsError[index];
const param = parseAbiType(input.type);
return (
<div key={ index } className={ styles.funcparams }>
<TypedInput
label={ label }
value={ value }
error={ error }
accounts={ accounts }
onChange={ onChange }
param={ param }
/>
</div>
);
});
return (
<div>
<p>Choose the contract parameters</p>
{ inputsComponents }
</div>
);
}
onParamChange = (index, value) => {
const { params, onParamsChange } = this.props;
params[index] = value;
onParamsChange(params);
}
}

View File

@ -31,3 +31,7 @@
.funcparams { .funcparams {
padding-left: 3em; padding-left: 3em;
} }
p {
color: rgba(255, 255, 255, 0.498039);
}

View File

@ -22,13 +22,19 @@ import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal,
import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep';
import ErrorStep from './ErrorStep'; import ErrorStep from './ErrorStep';
import styles from './deployContract.css'; import styles from './deployContract.css';
import { ERROR_CODES } from '../../api/transport/error'; import { ERROR_CODES } from '../../api/transport/error';
const steps = ['contract details', 'deployment', 'completed']; const STEPS = {
CONTRACT_DETAILS: { title: 'contract details' },
CONTRACT_PARAMETERS: { title: 'contract parameters' },
DEPLOYMENT: { title: 'deployment', waiting: true },
COMPLETED: { title: 'completed' }
};
export default class DeployContract extends Component { export default class DeployContract extends Component {
static contextTypes = { static contextTypes = {
@ -55,7 +61,6 @@ export default class DeployContract extends Component {
abiError: ERRORS.invalidAbi, abiError: ERRORS.invalidAbi,
code: '', code: '',
codeError: ERRORS.invalidCode, codeError: ERRORS.invalidCode,
deployState: '',
description: '', description: '',
descriptionError: null, descriptionError: null,
fromAddress: Object.keys(this.props.accounts)[0], fromAddress: Object.keys(this.props.accounts)[0],
@ -64,9 +69,12 @@ export default class DeployContract extends Component {
nameError: ERRORS.invalidName, nameError: ERRORS.invalidName,
params: [], params: [],
paramsError: [], paramsError: [],
step: 0, inputs: [],
deployState: '',
deployError: null, deployError: null,
rejected: false rejected: false,
step: 'CONTRACT_DETAILS'
} }
componentWillMount () { componentWillMount () {
@ -95,20 +103,30 @@ export default class DeployContract extends Component {
} }
render () { render () {
const { step, deployError, rejected } = this.state; const { step, deployError, rejected, inputs } = this.state;
const realStep = Object.keys(STEPS).findIndex((k) => k === step);
const realSteps = deployError || rejected
? null
: Object.keys(STEPS)
.filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0)
.map((k) => STEPS[k]);
const realSteps = deployError || rejected ? null : steps;
const title = realSteps const title = realSteps
? null ? null
: (deployError ? 'deployment failed' : 'rejected'); : (deployError ? 'deployment failed' : 'rejected');
const waiting = realSteps
? realSteps.map((s, i) => s.waiting ? i : false).filter((v) => v !== false)
: null;
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
current={ step } current={ realStep }
steps={ realSteps } steps={ realSteps ? realSteps.map((s) => s.title) : null }
title={ title } title={ title }
waiting={ realSteps ? [1] : null } waiting={ waiting }
visible visible
scroll> scroll>
{ this.renderStep() } { this.renderStep() }
@ -146,20 +164,29 @@ export default class DeployContract extends Component {
} }
switch (step) { switch (step) {
case 0: case 'CONTRACT_DETAILS':
return [ return [
cancelBtn, cancelBtn,
<Button <Button
disabled={ !isValid } disabled={ !isValid }
icon={ <IdentityIcon button address={ fromAddress } /> }
label='Next'
onClick={ this.onParametersStep } />
];
case 'CONTRACT_PARAMETERS':
return [
cancelBtn,
<Button
icon={ <IdentityIcon button address={ fromAddress } /> } icon={ <IdentityIcon button address={ fromAddress } /> }
label='Create' label='Create'
onClick={ this.onDeployStart } /> onClick={ this.onDeployStart } />
]; ];
case 1: case 'DEPLOYMENT':
return [ closeBtn ]; return [ closeBtn ];
case 2: case 'COMPLETED':
return [ closeBtnOk ]; return [ closeBtnOk ];
} }
} }
@ -184,21 +211,33 @@ export default class DeployContract extends Component {
} }
switch (step) { switch (step) {
case 0: case 'CONTRACT_DETAILS':
return ( return (
<DetailsStep <DetailsStep
{ ...this.state } { ...this.state }
readOnly={ readOnly }
accounts={ accounts } accounts={ accounts }
onAbiChange={ this.onAbiChange } readOnly={ readOnly }
onCodeChange={ this.onCodeChange }
onFromAddressChange={ this.onFromAddressChange } onFromAddressChange={ this.onFromAddressChange }
onDescriptionChange={ this.onDescriptionChange } onDescriptionChange={ this.onDescriptionChange }
onNameChange={ this.onNameChange } onNameChange={ this.onNameChange }
onParamsChange={ this.onParamsChange } /> onAbiChange={ this.onAbiChange }
onCodeChange={ this.onCodeChange }
onParamsChange={ this.onParamsChange }
onInputsChange={ this.onInputsChange }
/>
); );
case 1: case 'CONTRACT_PARAMETERS':
return (
<ParametersStep
{ ...this.state }
readOnly={ readOnly }
accounts={ accounts }
onParamsChange={ this.onParamsChange }
/>
);
case 'DEPLOYMENT':
const body = txhash const body = txhash
? <TxHash hash={ txhash } /> ? <TxHash hash={ txhash } />
: null; : null;
@ -210,7 +249,7 @@ export default class DeployContract extends Component {
</BusyStep> </BusyStep>
); );
case 2: case 'COMPLETED':
return ( return (
<CompletedStep> <CompletedStep>
<div>Your contract has been deployed at</div> <div>Your contract has been deployed at</div>
@ -225,12 +264,23 @@ export default class DeployContract extends Component {
} }
} }
onParametersStep = () => {
const { inputs } = this.state;
if (inputs.length) {
return this.setState({ step: 'CONTRACT_PARAMETERS' });
}
return this.onDeployStart();
}
onDescriptionChange = (description) => { onDescriptionChange = (description) => {
this.setState({ description, descriptionError: null }); this.setState({ description, descriptionError: null });
} }
onFromAddressChange = (fromAddress) => { onFromAddressChange = (fromAddress) => {
const { api } = this.context; const { api } = this.context;
const fromAddressError = api.util.isAddressValid(fromAddress) const fromAddressError = api.util.isAddressValid(fromAddress)
? null ? null
: 'a valid account as the contract owner needs to be selected'; : 'a valid account as the contract owner needs to be selected';
@ -246,6 +296,10 @@ export default class DeployContract extends Component {
this.setState({ params }); this.setState({ params });
} }
onInputsChange = (inputs) => {
this.setState({ inputs });
}
onAbiChange = (abi) => { onAbiChange = (abi) => {
const { api } = this.context; const { api } = this.context;
@ -267,7 +321,7 @@ export default class DeployContract extends Component {
from: fromAddress from: fromAddress
}; };
this.setState({ step: 1 }); this.setState({ step: 'DEPLOYMENT' });
api api
.newContract(abiParsed) .newContract(abiParsed)
@ -286,7 +340,7 @@ export default class DeployContract extends Component {
]) ])
.then(() => { .then(() => {
console.log(`contract deployed at ${address}`); console.log(`contract deployed at ${address}`);
this.setState({ step: 2, address }); this.setState({ step: 'DEPLOYMENT', address });
}); });
}) })
.catch((error) => { .catch((error) => {

View File

@ -16,7 +16,7 @@
import { newError } from '../ui/Errors/actions'; import { newError } from '../ui/Errors/actions';
import { setAddressImage } from './providers/imagesActions'; import { setAddressImage } from './providers/imagesActions';
import { clearStatusLogs, toggleStatusLogs } from './providers/statusActions'; import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
import { toggleView } from '../views/Settings'; import { toggleView } from '../views/Settings';
export { export {
@ -24,5 +24,6 @@ export {
clearStatusLogs, clearStatusLogs,
setAddressImage, setAddressImage,
toggleStatusLogs, toggleStatusLogs,
toggleStatusRefresh,
toggleView toggleView
}; };

View File

@ -15,32 +15,29 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
import { isEqual } from 'lodash';
export default class Status { export default class Status {
constructor (store, api) { constructor (store, api) {
this._api = api; this._api = api;
this._store = store; this._store = store;
this._pingable = false;
this._apiStatus = {};
this._status = {};
this._longStatus = {};
this._minerSettings = {};
this._pollPingTimeoutId = null;
this._longStatusTimeoutId = null;
} }
start () { start () {
this._subscribeBlockNumber(); this._subscribeBlockNumber();
this._pollPing(); this._pollPing();
this._pollStatus(); this._pollStatus();
this._pollLongStatus();
this._pollLogs(); this._pollLogs();
this._fetchEnode();
}
_fetchEnode () {
this._api.parity
.enode()
.then((enode) => {
this._store.dispatch(statusCollection({ enode }));
})
.catch(() => {
window.setTimeout(() => {
this._fetchEnode();
}, 1000);
});
} }
_subscribeBlockNumber () { _subscribeBlockNumber () {
@ -51,16 +48,58 @@ export default class Status {
} }
this._store.dispatch(statusBlockNumber(blockNumber)); this._store.dispatch(statusBlockNumber(blockNumber));
this._api.eth
.getBlockByNumber(blockNumber)
.then((block) => {
this._store.dispatch(statusCollection({ gasLimit: block.gasLimit }));
})
.catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
});
}) })
.then((subscriptionId) => { .then((subscriptionId) => {
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId); console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
}); });
} }
/**
* Pinging should be smart. It should only
* be used when the UI is connecting or the
* Node is deconnected.
*
* @see src/views/Connection/connection.js
*/
_shouldPing = () => {
const { isConnected, isConnecting } = this._apiStatus;
return isConnecting || !isConnected;
}
_stopPollPing = () => {
if (!this._pollPingTimeoutId) {
return;
}
clearTimeout(this._pollPingTimeoutId);
this._pollPingTimeoutId = null;
}
_pollPing = () => { _pollPing = () => {
const dispatch = (status, timeout = 500) => { // Already pinging, don't try again
this._store.dispatch(statusCollection({ isPingable: status })); if (this._pollPingTimeoutId) {
setTimeout(this._pollPing, timeout); return;
}
const dispatch = (pingable, timeout = 1000) => {
if (pingable !== this._pingable) {
this._pingable = pingable;
this._store.dispatch(statusCollection({ isPingable: pingable }));
}
this._pollPingTimeoutId = setTimeout(() => {
this._stopPollPing();
this._pollPing();
}, timeout);
}; };
fetch('/', { method: 'HEAD' }) fetch('/', { method: 'HEAD' })
@ -79,61 +118,162 @@ export default class Status {
} }
_pollStatus = () => { _pollStatus = () => {
const { secureToken, isConnected, isConnecting, needsToken } = this._api;
const nextTimeout = (timeout = 1000) => { const nextTimeout = (timeout = 1000) => {
setTimeout(this._pollStatus, timeout); setTimeout(this._pollStatus, timeout);
}; };
this._store.dispatch(statusCollection({ isConnected, isConnecting, needsToken, secureToken })); const { isConnected, isConnecting, needsToken, secureToken } = this._api;
const apiStatus = {
isConnected,
isConnecting,
needsToken,
secureToken
};
const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected;
if (gotReconnected) {
this._pollLongStatus();
}
if (!isEqual(apiStatus, this._apiStatus)) {
this._store.dispatch(statusCollection(apiStatus));
this._apiStatus = apiStatus;
}
// Ping if necessary, otherwise stop pinging
if (this._shouldPing()) {
this._pollPing();
} else {
this._stopPollPing();
}
if (!isConnected) { if (!isConnected) {
nextTimeout(250); return nextTimeout(250);
return;
} }
const { refreshStatus } = this._store.getState().nodeStatus;
const statusPromises = [ this._api.eth.syncing() ];
if (refreshStatus) {
statusPromises.push(this._api.eth.hashrate());
statusPromises.push(this._api.parity.netPeers());
}
Promise
.all(statusPromises)
.then((statusResults) => {
const status = statusResults.length === 1
? {
syncing: statusResults[0]
}
: {
syncing: statusResults[0],
hashrate: statusResults[1],
netPeers: statusResults[2]
};
if (!isEqual(status, this._status)) {
this._store.dispatch(statusCollection(status));
this._status = status;
}
nextTimeout();
})
.catch((error) => {
console.error('_pollStatus', error);
nextTimeout(250);
});
}
/**
* Miner settings should never changes unless
* Parity is restarted, or if the values are changed
* from the UI
*/
_pollMinerSettings = () => {
Promise
.all([
this._api.eth.coinbase(),
this._api.parity.extraData(),
this._api.parity.minGasPrice(),
this._api.parity.gasFloorTarget()
])
.then(([
coinbase, extraData, minGasPrice, gasFloorTarget
]) => {
const minerSettings = {
coinbase,
extraData,
minGasPrice,
gasFloorTarget
};
if (!isEqual(minerSettings, this._minerSettings)) {
this._store.dispatch(statusCollection(minerSettings));
this._minerSettings = minerSettings;
}
})
.catch((error) => {
console.error('_pollMinerSettings', error);
});
}
/**
* The data fetched here should not change
* unless Parity is restarted. They are thus
* fetched every 30s just in case, and whenever
* the client got reconnected.
*/
_pollLongStatus = () => {
const nextTimeout = (timeout = 30000) => {
if (this._longStatusTimeoutId) {
clearTimeout(this._longStatusTimeoutId);
}
this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout);
};
// Poll Miner settings just in case
this._pollMinerSettings();
Promise Promise
.all([ .all([
this._api.web3.clientVersion(), this._api.web3.clientVersion(),
this._api.eth.coinbase(),
this._api.parity.defaultExtraData(), this._api.parity.defaultExtraData(),
this._api.parity.extraData(),
this._api.parity.gasFloorTarget(),
this._api.eth.hashrate(),
this._api.parity.minGasPrice(),
this._api.parity.netChain(), this._api.parity.netChain(),
this._api.parity.netPeers(),
this._api.parity.netPort(), this._api.parity.netPort(),
this._api.parity.nodeName(),
this._api.parity.rpcSettings(), this._api.parity.rpcSettings(),
this._api.eth.syncing() this._api.parity.enode()
]) ])
.then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing, traceMode]) => { .then(([
clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
]) => {
const isTest = netChain === 'morden' || netChain === 'testnet'; const isTest = netChain === 'morden' || netChain === 'testnet';
this._store.dispatch(statusCollection({ const longStatus = {
clientVersion, clientVersion,
coinbase,
defaultExtraData, defaultExtraData,
extraData,
gasFloorTarget,
hashrate,
minGasPrice,
netChain, netChain,
netPeers,
netPort, netPort,
nodeName,
rpcSettings, rpcSettings,
syncing, enode,
isTest, isTest
traceMode };
}));
}) if (!isEqual(longStatus, this._longStatus)) {
.catch((error) => { this._store.dispatch(statusCollection(longStatus));
console.error('_pollStatus', error); this._longStatus = longStatus;
}); }
nextTimeout(); nextTimeout();
})
.catch((error) => {
console.error('_pollLongStatus', error);
nextTimeout(250);
});
} }
_pollLogs = () => { _pollLogs = () => {

View File

@ -47,3 +47,10 @@ export function clearStatusLogs () {
type: 'clearStatusLogs' type: 'clearStatusLogs'
}; };
} }
export function toggleStatusRefresh (refreshStatus) {
return {
type: 'toggleStatusRefresh',
refreshStatus
};
}

View File

@ -28,6 +28,7 @@ const initialState = {
enode: '', enode: '',
extraData: '', extraData: '',
gasFloorTarget: new BigNumber(0), gasFloorTarget: new BigNumber(0),
gasLimit: new BigNumber(0),
hashrate: new BigNumber(0), hashrate: new BigNumber(0),
minGasPrice: new BigNumber(0), minGasPrice: new BigNumber(0),
netChain: 'morden', netChain: 'morden',
@ -37,12 +38,13 @@ const initialState = {
max: new BigNumber(0) max: new BigNumber(0)
}, },
netPort: new BigNumber(0), netPort: new BigNumber(0),
nodeName: '',
rpcSettings: {}, rpcSettings: {},
syncing: false, syncing: false,
isApiConnected: true, isConnected: false,
isPingConnected: true, isConnecting: false,
isPingable: false,
isTest: false, isTest: false,
refreshStatus: false,
traceMode: undefined traceMode: undefined
}; };
@ -73,5 +75,10 @@ export default handleActions({
clearStatusLogs (state, action) { clearStatusLogs (state, action) {
return Object.assign({}, state, { devLogs: [] }); return Object.assign({}, state, { devLogs: [] });
},
toggleStatusRefresh (state, action) {
const { refreshStatus } = action;
return Object.assign({}, state, { refreshStatus });
} }
}, initialState); }, initialState);

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './radioButtons';

View File

@ -0,0 +1,32 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.spaced {
margin: 0.25em 0;
}
.typeContainer {
display: flex;
flex-direction: column;
.desc {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
}
}

View File

@ -0,0 +1,100 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import styles from './radioButtons.css';
export default class RadioButtons extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
value: PropTypes.any,
name: PropTypes.string
};
static defaultProps = {
value: 0,
name: ''
};
render () {
const { value, values } = this.props;
const index = parseInt(value);
const selectedValue = typeof value !== 'object' ? values[index] : value;
const key = this.getKey(selectedValue, index);
return (
<RadioButtonGroup
valueSelected={ key }
name={ name }
onChange={ this.onChange }
>
{ this.renderContent() }
</RadioButtonGroup>
);
}
renderContent () {
const { values } = this.props;
return values.map((value, index) => {
const label = typeof value === 'string' ? value : value.label || '';
const description = (typeof value !== 'string' && value.description) || null;
const key = this.getKey(value, index);
return (
<RadioButton
className={ styles.spaced }
key={ index }
value={ key }
label={ (
<div className={ styles.typeContainer }>
<span>{ label }</span>
{
description
? (
<span className={ styles.desc }>{ description }</span>
)
: null
}
</div>
) }
/>
);
});
}
getKey (value, index) {
if (typeof value !== 'string') {
return typeof value.key === 'undefined' ? index : value.key;
}
return index;
}
onChange = (event, index) => {
const { onChange, values } = this.props;
const value = values[index] || values.find((v) => v.key === index);
onChange(value, index);
}
}

View File

@ -23,6 +23,7 @@ import InputAddressSelect from './InputAddressSelect';
import InputChip from './InputChip'; import InputChip from './InputChip';
import InputInline from './InputInline'; import InputInline from './InputInline';
import Select from './Select'; import Select from './Select';
import RadioButtons from './RadioButtons';
export default from './form'; export default from './form';
export { export {
@ -34,5 +35,6 @@ export {
InputAddressSelect, InputAddressSelect,
InputChip, InputChip,
InputInline, InputInline,
Select Select,
RadioButtons
}; };

View File

@ -43,7 +43,7 @@ class Modal extends Component {
waiting: PropTypes.array, waiting: PropTypes.array,
scroll: PropTypes.bool, scroll: PropTypes.bool,
steps: PropTypes.array, steps: PropTypes.array,
title: React.PropTypes.oneOfType([ title: PropTypes.oneOfType([
PropTypes.node, PropTypes.string PropTypes.node, PropTypes.string
]), ]),
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,

View File

@ -19,5 +19,5 @@
} }
.layout>div { .layout>div {
padding-bottom: 0.25em; padding-bottom: 0.75em;
} }

View File

@ -29,7 +29,7 @@ import ContextProvider from './ContextProvider';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
import Editor from './Editor'; import Editor from './Editor';
import Errors from './Errors'; import Errors from './Errors';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import MethodDecoding from './MethodDecoding'; import MethodDecoding from './MethodDecoding';
@ -78,6 +78,7 @@ export {
muiTheme, muiTheme,
Page, Page,
ParityBackground, ParityBackground,
RadioButtons,
SignerIcon, SignerIcon,
Tags, Tags,
Tooltip, Tooltip,

View File

@ -37,7 +37,7 @@ export function validateAbi (abi, api) {
try { try {
abiParsed = JSON.parse(abi); abiParsed = JSON.parse(abi);
if (!api.util.isArray(abiParsed) || !abiParsed.length) { if (!api.util.isArray(abiParsed)) {
abiError = ERRORS.invalidAbi; abiError = ERRORS.invalidAbi;
return { abi, abiError, abiParsed }; return { abi, abiError, abiParsed };
} }

View File

@ -18,3 +18,22 @@
.description { .description {
margin-top: .5em !important; margin-top: .5em !important;
} }
.list {
.background {
background: rgba(255, 255, 255, 0.2);
margin: 0 -1.5em;
padding: 0.5em 1.5em;
}
.header {
text-transform: uppercase;
}
.byline {
font-size: 0.75em;
padding-top: 0.5em;
line-height: 1.5em;
opacity: 0.75;
}
}

View File

@ -51,16 +51,37 @@ export default class AddDapps extends Component {
] } ] }
visible visible
scroll> scroll>
<List> <div className={ styles.warning }>
{ store.apps.map(this.renderApp) } </div>
</List> { this.renderList(store.sortedLocal, 'Applications locally available', 'All applications installed locally on the machine by the user for access by the Parity client.') }
{ this.renderList(store.sortedBuiltin, 'Applications bundled with Parity', '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, 'Applications on the global network', '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> </Modal>
); );
} }
renderList (items, header, byline) {
if (!items || !items.length) {
return null;
}
return (
<div className={ styles.list }>
<div className={ styles.background }>
<div className={ styles.header }>{ header }</div>
<div className={ styles.byline }>{ byline }</div>
</div>
<List>
{ items.map(this.renderApp) }
</List>
</div>
);
}
renderApp = (app) => { renderApp = (app) => {
const { store } = this.props; const { store } = this.props;
const isHidden = store.hidden.includes(app.id); const isHidden = !store.displayApps[app.id].visible;
const onCheck = () => { const onCheck = () => {
if (isHidden) { if (isHidden) {
store.showApp(app.id); store.showApp(app.id);

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { Container, ContainerTitle } from '../../../ui'; import { Container, ContainerTitle, Tags } from '../../../ui';
import styles from './summary.css'; import styles from './summary.css';
@ -49,6 +49,7 @@ export default class Summary extends Component {
return ( return (
<Container className={ styles.container }> <Container className={ styles.container }>
{ image } { image }
<Tags tags={ [app.type] } />
<div className={ styles.description }> <div className={ styles.description }>
<ContainerTitle <ContainerTitle
className={ styles.title } className={ styles.title }

View File

@ -5,7 +5,8 @@
"name": "Token Deployment", "name": "Token Deployment",
"description": "Deploy new basic tokens that you are able to send around", "description": "Deploy new basic tokens that you are able to send around",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0" "version": "1.0.0",
"visible": true
}, },
{ {
"id": "0xd1adaede68d344519025e2ff574650cd99d3830fe6d274c7a7843cdc00e17938", "id": "0xd1adaede68d344519025e2ff574650cd99d3830fe6d274c7a7843cdc00e17938",
@ -13,7 +14,8 @@
"name": "Registry", "name": "Registry",
"description": "A global registry of addresses on the network", "description": "A global registry of addresses on the network",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0" "version": "1.0.0",
"visible": true
}, },
{ {
"id": "0x0a8048117e51e964628d0f2d26342b3cd915248b59bcce2721e1d05f5cfa2208", "id": "0x0a8048117e51e964628d0f2d26342b3cd915248b59bcce2721e1d05f5cfa2208",
@ -21,7 +23,8 @@
"name": "Token Registry", "name": "Token Registry",
"description": "A registry of transactable tokens on the network", "description": "A registry of transactable tokens on the network",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0" "version": "1.0.0",
"visible": true
}, },
{ {
"id": "0xf49089046f53f5d2e5f3513c1c32f5ff57d986e46309a42d2b249070e4e72c46", "id": "0xf49089046f53f5d2e5f3513c1c32f5ff57d986e46309a42d2b249070e4e72c46",
@ -29,7 +32,8 @@
"name": "Method Registry", "name": "Method Registry",
"description": "A registry of method signatures for lookups on transactions", "description": "A registry of method signatures for lookups on transactions",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0" "version": "1.0.0",
"visible": true
}, },
{ {
"id": "0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75", "id": "0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75",
@ -38,6 +42,16 @@
"description": "A mapping of GitHub URLs to hashes for use in contracts as references", "description": "A mapping of GitHub URLs to hashes for use in contracts as references",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0", "version": "1.0.0",
"visible": false,
"secure": true
},
{
"id": "0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d",
"url": "localtx",
"name": "TxQueue Viewer",
"description": "Have a peak on internals of transaction queue of your node.",
"author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0",
"secure": true "secure": true
} }
] ]

View File

@ -18,6 +18,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: -0.125em; margin: -0.125em;
position: relative;
} }
.list+.list { .list+.list {
@ -29,3 +30,25 @@
flex: 0 1 50%; flex: 0 1 50%;
box-sizing: border-box; box-sizing: border-box;
} }
.overlay {
background: rgba(0, 0, 0, 0.85);
bottom: 0.5em;
left: -0.125em;
position: absolute;
right: -0.125em;
top: -0.25em;
z-index: 100;
padding: 1em;
.body {
line-height: 1.5em;
margin: 0 auto;
text-align: left;
max-width: 980px;
&>div:first-child {
padding-bottom: 1em;
}
}
}

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Checkbox } from 'material-ui';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Actionbar, Page } from '../../ui'; import { Actionbar, Page } from '../../ui';
@ -37,6 +38,24 @@ export default class Dapps extends Component {
store = new DappsStore(this.context.api); store = new DappsStore(this.context.api);
render () { render () {
let externalOverlay = null;
if (this.store.externalOverlayVisible) {
externalOverlay = (
<div className={ styles.overlay }>
<div className={ styles.body }>
<div>Applications made available on the network by 3rd-party authors 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 before interacting.</div>
<div>
<Checkbox
className={ styles.accept }
label='I understand that these applications are not affiliated with Parity'
checked={ false }
onCheck={ this.onClickAcceptExternal } />
</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<AddDapps store={ this.store } /> <AddDapps store={ this.store } />
@ -53,14 +72,27 @@ export default class Dapps extends Component {
] } ] }
/> />
<Page> <Page>
<div className={ styles.list }> { this.renderList(this.store.visibleLocal) }
{ this.store.visible.map(this.renderApp) } { this.renderList(this.store.visibleBuiltin) }
</div> { this.renderList(this.store.visibleNetwork, externalOverlay) }
</Page> </Page>
</div> </div>
); );
} }
renderList (items, overlay) {
if (!items || !items.length) {
return null;
}
return (
<div className={ styles.list }>
{ overlay }
{ items.map(this.renderApp) }
</div>
);
}
renderApp = (app) => { renderApp = (app) => {
return ( return (
<div <div
@ -70,4 +102,8 @@ export default class Dapps extends Component {
</div> </div>
); );
} }
onClickAcceptExternal = () => {
this.store.closeExternalOverlay();
}
} }

View File

@ -14,39 +14,65 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { action, computed, observable, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import store from 'store';
import Contracts from '../../contracts'; import Contracts from '../../contracts';
import { hashToImageUrl } from '../../redux/util'; import { hashToImageUrl } from '../../redux/util';
import builtinApps from './builtin.json'; import builtinApps from './builtin.json';
const LS_KEY_HIDDEN = 'hiddenApps'; const LS_KEY_DISPLAY = 'displayApps';
const LS_KEY_EXTERNAL = 'externalApps'; const LS_KEY_EXTERNAL_ACCEPT = 'acceptExternal';
export default class DappsStore { export default class DappsStore {
@observable apps = []; @observable apps = [];
@observable externalApps = []; @observable displayApps = {};
@observable hiddenApps = [];
@observable modalOpen = false; @observable modalOpen = false;
@observable externalOverlayVisible = true;
constructor (api) { constructor (api) {
this._api = api; this._api = api;
this._readHiddenApps(); this.loadExternalOverlay();
this._readExternalApps(); this.readDisplayApps();
this._fetchBuiltinApps(); Promise
this._fetchLocalApps(); .all([
this._fetchRegistryApps(); this._fetchBuiltinApps(),
this._fetchLocalApps(),
this._fetchRegistryApps()
])
.then(this.writeDisplayApps);
} }
@computed get visible () { @computed get sortedBuiltin () {
return this.apps return this.apps.filter((app) => app.type === 'builtin');
.filter((app) => { }
return this.externalApps.includes(app.id) || !this.hiddenApps.includes(app.id);
}) @computed get sortedLocal () {
.sort((a, b) => a.name.localeCompare(b.name)); return this.apps.filter((app) => app.type === 'local');
}
@computed get sortedNetwork () {
return this.apps.filter((app) => app.type === 'network');
}
@computed get visibleApps () {
return this.apps.filter((app) => this.displayApps[app.id] && this.displayApps[app.id].visible);
}
@computed get visibleBuiltin () {
return this.visibleApps.filter((app) => app.type === 'builtin');
}
@computed get visibleLocal () {
return this.visibleApps.filter((app) => app.type === 'local');
}
@computed get visibleNetwork () {
return this.visibleApps.filter((app) => app.type === 'network');
} }
@action openModal = () => { @action openModal = () => {
@ -57,14 +83,48 @@ export default class DappsStore {
this.modalOpen = false; this.modalOpen = false;
} }
@action closeExternalOverlay = () => {
this.externalOverlayVisible = false;
store.set(LS_KEY_EXTERNAL_ACCEPT, true);
}
@action loadExternalOverlay () {
this.externalOverlayVisible = !(store.get(LS_KEY_EXTERNAL_ACCEPT) || false);
}
@action hideApp = (id) => { @action hideApp = (id) => {
this.hiddenApps = this.hiddenApps.concat(id); this.displayApps = Object.assign({}, this.displayApps, { [id]: { visible: false } });
this._writeHiddenApps(); this.writeDisplayApps();
} }
@action showApp = (id) => { @action showApp = (id) => {
this.hiddenApps = this.hiddenApps.filter((_id) => _id !== id); this.displayApps = Object.assign({}, this.displayApps, { [id]: { visible: true } });
this._writeHiddenApps(); this.writeDisplayApps();
}
@action readDisplayApps = () => {
this.displayApps = store.get(LS_KEY_DISPLAY) || {};
}
@action writeDisplayApps = () => {
store.set(LS_KEY_DISPLAY, this.displayApps);
}
@action addApps = (apps) => {
transaction(() => {
this.apps = this.apps
.concat(apps || [])
.sort((a, b) => a.name.localeCompare(b.name));
const visibility = {};
apps.forEach((app) => {
if (!this.displayApps[app.id]) {
visibility[app.id] = { visible: app.visible };
}
});
this.displayApps = Object.assign({}, this.displayApps, visibility);
});
} }
_getHost (api) { _getHost (api) {
@ -79,13 +139,16 @@ export default class DappsStore {
return Promise return Promise
.all(builtinApps.map((app) => dappReg.getImage(app.id))) .all(builtinApps.map((app) => dappReg.getImage(app.id)))
.then((imageIds) => { .then((imageIds) => {
transaction(() => { this.addApps(
builtinApps.forEach((app, index) => { builtinApps.map((app, index) => {
app.type = 'builtin'; app.type = 'builtin';
app.image = hashToImageUrl(imageIds[index]); app.image = hashToImageUrl(imageIds[index]);
this.apps.push(app); return app;
}); })
}); );
})
.catch((error) => {
console.warn('DappsStore:fetchBuiltinApps', error);
}); });
} }
@ -100,15 +163,12 @@ export default class DappsStore {
return apps return apps
.map((app) => { .map((app) => {
app.type = 'local'; app.type = 'local';
app.visible = true;
return app; return app;
}) })
.filter((app) => app.id && !['ui'].includes(app.id)); .filter((app) => app.id && !['ui'].includes(app.id));
}) })
.then((apps) => { .then(this.addApps)
transaction(() => {
(apps || []).forEach((app) => this.apps.push(app));
});
})
.catch((error) => { .catch((error) => {
console.warn('DappsStore:fetchLocal', error); console.warn('DappsStore:fetchLocal', error);
}); });
@ -132,7 +192,9 @@ export default class DappsStore {
.then((appsInfo) => { .then((appsInfo) => {
const appIds = appsInfo const appIds = appsInfo
.map(([appId, owner]) => this._api.util.bytesToHex(appId)) .map(([appId, owner]) => this._api.util.bytesToHex(appId))
.filter((appId) => !builtinApps.find((app) => app.id === appId)); .filter((appId) => {
return (new BigNumber(appId)).gt(0) && !builtinApps.find((app) => app.id === appId);
});
return Promise return Promise
.all([ .all([
@ -147,7 +209,8 @@ export default class DappsStore {
image: hashToImageUrl(imageIds[index]), image: hashToImageUrl(imageIds[index]),
contentHash: this._api.util.bytesToHex(contentIds[index]).substr(2), contentHash: this._api.util.bytesToHex(contentIds[index]).substr(2),
manifestHash: this._api.util.bytesToHex(manifestIds[index]).substr(2), manifestHash: this._api.util.bytesToHex(manifestIds[index]).substr(2),
type: 'network' type: 'network',
visible: true
}; };
return app; return app;
@ -179,11 +242,7 @@ export default class DappsStore {
}); });
}); });
}) })
.then((apps) => { .then(this.addApps)
transaction(() => {
(apps || []).forEach((app) => this.apps.push(app));
});
})
.catch((error) => { .catch((error) => {
console.warn('DappsStore:fetchRegistry', error); console.warn('DappsStore:fetchRegistry', error);
}); });
@ -201,44 +260,4 @@ export default class DappsStore {
return null; return null;
}); });
} }
_readHiddenApps () {
const stored = localStorage.getItem(LS_KEY_HIDDEN);
if (stored) {
try {
this.hiddenApps = JSON.parse(stored);
} catch (error) {
console.warn('DappsStore:readHiddenApps', error);
}
}
}
_readExternalApps () {
const stored = localStorage.getItem(LS_KEY_EXTERNAL);
if (stored) {
try {
this.externalApps = JSON.parse(stored);
} catch (error) {
console.warn('DappsStore:readExternalApps', error);
}
}
}
_writeExternalApps () {
try {
localStorage.setItem(LS_KEY_EXTERNAL, JSON.stringify(this.externalApps));
} catch (error) {
console.error('DappsStore:writeExternalApps', error);
}
}
_writeHiddenApps () {
try {
localStorage.setItem(LS_KEY_HIDDEN, JSON.stringify(this.hiddenApps));
} catch (error) {
console.error('DappsStore:writeHiddenApps', error);
}
}
} }

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearStatusLogs, toggleStatusLogs } from '../../../../redux/actions'; import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '../../../../redux/actions';
import Debug from '../../components/Debug'; import Debug from '../../components/Debug';
import Status from '../../components/Status'; import Status from '../../components/Status';
@ -31,6 +31,14 @@ class StatusPage extends Component {
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired
} }
componentWillMount () {
this.props.actions.toggleStatusRefresh(true);
}
componentWillUnmount () {
this.props.actions.toggleStatusRefresh(false);
}
render () { render () {
return ( return (
<div className={ styles.body }> <div className={ styles.body }>
@ -49,7 +57,8 @@ function mapDispatchToProps (dispatch) {
return { return {
actions: bindActionCreators({ actions: bindActionCreators({
clearStatusLogs, clearStatusLogs,
toggleStatusLogs toggleStatusLogs,
toggleStatusRefresh
}, dispatch) }, dispatch)
}; };
} }

45
js/test/npmLibrary.js Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
try {
var Api = require('../.npmjs/library.js').Api;
var Abi = require('../.npmjs/library.js').Abi;
if (typeof Api !== 'function') {
throw new Error('No Api');
}
if (typeof Abi !== 'function') {
throw new Error('No Abi');
}
var transport = new Api.Transport.Http('http://localhost:8545');
var api = new Api(transport);
api.eth
.blockNumber()
.then((block) => {
console.log('library working fine', '(block #' + block.toFormat() + ')');
process.exit(0);
})
.catch(() => {
console.log('library working fine (disconnected)');
process.exit(0);
});
} catch (e) {
console.error('An error occured:', e.toString().split('\n')[0]);
process.exit(1);
}

View File

@ -40,6 +40,7 @@ module.exports = {
'githubhint': ['./dapps/githubhint.js'], 'githubhint': ['./dapps/githubhint.js'],
'registry': ['./dapps/registry.js'], 'registry': ['./dapps/registry.js'],
'signaturereg': ['./dapps/signaturereg.js'], 'signaturereg': ['./dapps/signaturereg.js'],
'localtx': ['./dapps/localtx.js'],
'tokenreg': ['./dapps/tokenreg.js'], 'tokenreg': ['./dapps/tokenreg.js'],
// app // app
'index': ['./index.js'] 'index': ['./index.js']

View File

@ -34,7 +34,9 @@ module.exports = {
}, },
output: { output: {
path: path.join(__dirname, DEST), path: path.join(__dirname, DEST),
filename: '[name].js' filename: '[name].js',
library: '[name].js',
libraryTarget: 'umd'
}, },
module: { module: {
loaders: [ loaders: [

View File

@ -24,13 +24,23 @@ const isProd = ENV === 'production';
module.exports = { module.exports = {
context: path.join(__dirname, './src'), context: path.join(__dirname, './src'),
target: 'node',
entry: 'library.js', entry: 'library.js',
output: { output: {
path: path.join(__dirname, '.npmjs'), path: path.join(__dirname, '.npmjs'),
filename: 'library.js', filename: 'library.js',
libraryTarget: 'commonjs' library: 'Parity',
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: {
'node-fetch': 'node-fetch',
'vertx': 'vertx'
}, },
module: { module: {
noParse: [
/babel-polyfill/
],
loaders: [ loaders: [
{ {
test: /(\.jsx|\.js)$/, test: /(\.jsx|\.js)$/,

View File

@ -110,7 +110,7 @@ mod server {
use rpc_apis; use rpc_apis;
use ethcore_rpc::is_major_importing; use ethcore_rpc::is_major_importing;
use ethcore_dapps::ContractClient; use hash_fetch::urlhint::ContractClient;
pub use ethcore_dapps::Server as WebappServer; pub use ethcore_dapps::Server as WebappServer;

View File

@ -42,6 +42,7 @@ extern crate ethcore_ipc_nano as nanoipc;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate rlp; extern crate rlp;
extern crate ethcore_hash_fetch as hash_fetch;
extern crate json_ipc_server as jsonipc; extern crate json_ipc_server as jsonipc;

View File

@ -28,6 +28,7 @@ use user_defaults::UserDefaults;
pub enum SpecType { pub enum SpecType {
Mainnet, Mainnet,
Testnet, Testnet,
Ropsten,
Olympic, Olympic,
Classic, Classic,
Expanse, Expanse,
@ -49,6 +50,7 @@ impl str::FromStr for SpecType {
"frontier" | "homestead" | "mainnet" => SpecType::Mainnet, "frontier" | "homestead" | "mainnet" => SpecType::Mainnet,
"frontier-dogmatic" | "homestead-dogmatic" | "classic" => SpecType::Classic, "frontier-dogmatic" | "homestead-dogmatic" | "classic" => SpecType::Classic,
"morden" | "testnet" => SpecType::Testnet, "morden" | "testnet" => SpecType::Testnet,
"ropsten" => SpecType::Ropsten,
"olympic" => SpecType::Olympic, "olympic" => SpecType::Olympic,
"expanse" => SpecType::Expanse, "expanse" => SpecType::Expanse,
"dev" => SpecType::Dev, "dev" => SpecType::Dev,
@ -63,6 +65,7 @@ impl SpecType {
match *self { match *self {
SpecType::Mainnet => Ok(ethereum::new_frontier()), SpecType::Mainnet => Ok(ethereum::new_frontier()),
SpecType::Testnet => Ok(ethereum::new_morden()), SpecType::Testnet => Ok(ethereum::new_morden()),
SpecType::Ropsten => Ok(ethereum::new_ropsten()),
SpecType::Olympic => Ok(ethereum::new_olympic()), SpecType::Olympic => Ok(ethereum::new_olympic()),
SpecType::Classic => Ok(ethereum::new_classic()), SpecType::Classic => Ok(ethereum::new_classic()),
SpecType::Expanse => Ok(ethereum::new_expanse()), SpecType::Expanse => Ok(ethereum::new_expanse()),
@ -285,6 +288,7 @@ mod tests {
assert_eq!(SpecType::Mainnet, "mainnet".parse().unwrap()); assert_eq!(SpecType::Mainnet, "mainnet".parse().unwrap());
assert_eq!(SpecType::Testnet, "testnet".parse().unwrap()); assert_eq!(SpecType::Testnet, "testnet".parse().unwrap());
assert_eq!(SpecType::Testnet, "morden".parse().unwrap()); assert_eq!(SpecType::Testnet, "morden".parse().unwrap());
assert_eq!(SpecType::Ropsten, "ropsten".parse().unwrap());
assert_eq!(SpecType::Olympic, "olympic".parse().unwrap()); assert_eq!(SpecType::Olympic, "olympic".parse().unwrap());
} }

View File

@ -28,6 +28,7 @@ use jsonrpc_core::Error;
use v1::helpers::{errors, TransactionRequest, FilledTransactionRequest, ConfirmationPayload}; use v1::helpers::{errors, TransactionRequest, FilledTransactionRequest, ConfirmationPayload};
use v1::types::{ use v1::types::{
H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes, H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes,
RichRawTransaction as RpcRichRawTransaction,
ConfirmationPayload as RpcConfirmationPayload, ConfirmationPayload as RpcConfirmationPayload,
ConfirmationResponse, ConfirmationResponse,
SignRequest as RpcSignRequest, SignRequest as RpcSignRequest,
@ -47,8 +48,7 @@ pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload:
}, },
ConfirmationPayload::SignTransaction(request) => { ConfirmationPayload::SignTransaction(request) => {
sign_no_dispatch(client, miner, accounts, request, pass) sign_no_dispatch(client, miner, accounts, request, pass)
.map(|tx| rlp::encode(&tx).to_vec()) .map(RpcRichRawTransaction::from)
.map(RpcBytes)
.map(ConfirmationResponse::SignTransaction) .map(ConfirmationResponse::SignTransaction)
}, },
ConfirmationPayload::Signature(address, hash) => { ConfirmationPayload::Signature(address, hash) => {

View File

@ -22,7 +22,7 @@ macro_rules! rpc_unimplemented {
use std::fmt; use std::fmt;
use rlp::DecoderError; use rlp::DecoderError;
use ethcore::error::{Error as EthcoreError, CallError}; use ethcore::error::{Error as EthcoreError, CallError, TransactionError};
use ethcore::account_provider::{Error as AccountError}; use ethcore::account_provider::{Error as AccountError};
use fetch::FetchError; use fetch::FetchError;
use jsonrpc_core::{Error, ErrorCode, Value}; use jsonrpc_core::{Error, ErrorCode, Value};
@ -227,11 +227,10 @@ pub fn from_password_error(error: AccountError) -> Error {
} }
} }
pub fn from_transaction_error(error: EthcoreError) -> Error { pub fn transaction_message(error: TransactionError) -> String {
use ethcore::error::TransactionError::*; use ethcore::error::TransactionError::*;
if let EthcoreError::Transaction(e) = error { match error {
let msg = match e {
AlreadyImported => "Transaction with the same hash was already imported.".into(), AlreadyImported => "Transaction with the same hash was already imported.".into(),
Old => "Transaction nonce is too low. Try incrementing the nonce.".into(), Old => "Transaction nonce is too low. Try incrementing the nonce.".into(),
TooCheapToReplace => { TooCheapToReplace => {
@ -252,15 +251,20 @@ pub fn from_transaction_error(error: EthcoreError) -> Error {
GasLimitExceeded { limit, got } => { GasLimitExceeded { limit, got } => {
format!("Transaction cost exceeds current gas limit. Limit: {}, got: {}. Try decreasing supplied gas.", limit, got) format!("Transaction cost exceeds current gas limit. Limit: {}, got: {}. Try decreasing supplied gas.", limit, got)
}, },
InvalidNetworkId => "Invalid network id.".into(),
InvalidGasLimit(_) => "Supplied gas is beyond limit.".into(), InvalidGasLimit(_) => "Supplied gas is beyond limit.".into(),
SenderBanned => "Sender is banned in local queue.".into(), SenderBanned => "Sender is banned in local queue.".into(),
RecipientBanned => "Recipient is banned in local queue.".into(), RecipientBanned => "Recipient is banned in local queue.".into(),
CodeBanned => "Code is banned in local queue.".into(), CodeBanned => "Code is banned in local queue.".into(),
e => format!("{}", e).into(), }
}; }
pub fn from_transaction_error(error: EthcoreError) -> Error {
if let EthcoreError::Transaction(e) = error {
Error { Error {
code: ErrorCode::ServerError(codes::TRANSACTION_ERROR), code: ErrorCode::ServerError(codes::TRANSACTION_ERROR),
message: msg, message: transaction_message(e),
data: None, data: None,
} }
} else { } else {

View File

@ -619,6 +619,10 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
} }
} }
fn submit_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> {
self.send_raw_transaction(raw)
}
fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> { fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> {
try!(self.active()); try!(self.active());

View File

@ -34,7 +34,11 @@ use ethcore::account_provider::AccountProvider;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::traits::Parity; use v1::traits::Parity;
use v1::types::{Bytes, U256, H160, H256, H512, Peers, Transaction, RpcSettings, Histogram}; use v1::types::{
Bytes, U256, H160, H256, H512,
Peers, Transaction, RpcSettings, Histogram,
TransactionStats, LocalTransactionStatus,
};
use v1::helpers::{errors, SigningQueue, SignerService, NetworkSettings}; use v1::helpers::{errors, SigningQueue, SignerService, NetworkSettings};
use v1::helpers::dispatch::DEFAULT_MAC; use v1::helpers::dispatch::DEFAULT_MAC;
@ -259,6 +263,27 @@ impl<C, M, S: ?Sized> Parity for ParityClient<C, M, S> where
Ok(take_weak!(self.miner).all_transactions().into_iter().map(Into::into).collect::<Vec<_>>()) Ok(take_weak!(self.miner).all_transactions().into_iter().map(Into::into).collect::<Vec<_>>())
} }
fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error> {
try!(self.active());
let stats = take_weak!(self.sync).transactions_stats();
Ok(stats.into_iter()
.map(|(hash, stats)| (hash.into(), stats.into()))
.collect()
)
}
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> {
try!(self.active());
let transactions = take_weak!(self.miner).local_transactions();
Ok(transactions
.into_iter()
.map(|(hash, status)| (hash.into(), status.into()))
.collect()
)
}
fn signer_port(&self) -> Result<u16, Error> { fn signer_port(&self) -> Result<u16, Error> {
try!(self.active()); try!(self.active());

View File

@ -34,6 +34,7 @@ use v1::traits::{EthSigning, ParitySigning};
use v1::types::{ use v1::types::{
H160 as RpcH160, H256 as RpcH256, U256 as RpcU256, Bytes as RpcBytes, H520 as RpcH520, H160 as RpcH160, H256 as RpcH256, U256 as RpcU256, Bytes as RpcBytes, H520 as RpcH520,
Either as RpcEither, Either as RpcEither,
RichRawTransaction as RpcRichRawTransaction,
TransactionRequest as RpcTransactionRequest, TransactionRequest as RpcTransactionRequest,
ConfirmationPayload as RpcConfirmationPayload, ConfirmationPayload as RpcConfirmationPayload,
ConfirmationResponse as RpcConfirmationResponse ConfirmationResponse as RpcConfirmationResponse
@ -201,11 +202,11 @@ impl<C: 'static, M: 'static> EthSigning for SigningQueueClient<C, M> where
}); });
} }
fn sign_transaction(&self, ready: Ready<RpcBytes>, request: RpcTransactionRequest) { fn sign_transaction(&self, ready: Ready<RpcRichRawTransaction>, request: RpcTransactionRequest) {
let res = self.active().and_then(|_| self.dispatch(RpcConfirmationPayload::SignTransaction(request))); let res = self.active().and_then(|_| self.dispatch(RpcConfirmationPayload::SignTransaction(request)));
self.handle_dispatch(res, |response| { self.handle_dispatch(res, |response| {
match response { match response {
Ok(RpcConfirmationResponse::SignTransaction(rlp)) => ready.ready(Ok(rlp)), Ok(RpcConfirmationResponse::SignTransaction(tx)) => ready.ready(Ok(tx)),
Err(e) => ready.ready(Err(e)), Err(e) => ready.ready(Err(e)),
e => ready.ready(Err(errors::internal("Unexpected result.", e))), e => ready.ready(Err(errors::internal("Unexpected result.", e))),
} }

View File

@ -31,6 +31,7 @@ use v1::types::{
U256 as RpcU256, U256 as RpcU256,
H160 as RpcH160, H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes, H160 as RpcH160, H256 as RpcH256, H520 as RpcH520, Bytes as RpcBytes,
Either as RpcEither, Either as RpcEither,
RichRawTransaction as RpcRichRawTransaction,
TransactionRequest as RpcTransactionRequest, TransactionRequest as RpcTransactionRequest,
ConfirmationPayload as RpcConfirmationPayload, ConfirmationPayload as RpcConfirmationPayload,
ConfirmationResponse as RpcConfirmationResponse, ConfirmationResponse as RpcConfirmationResponse,
@ -100,9 +101,9 @@ impl<C: 'static, M: 'static> EthSigning for SigningUnsafeClient<C, M> where
ready.ready(result); ready.ready(result);
} }
fn sign_transaction(&self, ready: Ready<RpcBytes>, request: RpcTransactionRequest) { fn sign_transaction(&self, ready: Ready<RpcRichRawTransaction>, request: RpcTransactionRequest) {
let result = match self.handle(RpcConfirmationPayload::SignTransaction(request)) { let result = match self.handle(RpcConfirmationPayload::SignTransaction(request)) {
Ok(RpcConfirmationResponse::SignTransaction(rlp)) => Ok(rlp), Ok(RpcConfirmationResponse::SignTransaction(tx)) => Ok(tx),
Err(e) => Err(e), Err(e) => Err(e),
e => Err(errors::internal("Unexpected result", e)), e => Err(errors::internal("Unexpected result", e)),
}; };

View File

@ -24,7 +24,7 @@ use ethcore::block::{ClosedBlock, IsBlock};
use ethcore::header::BlockNumber; use ethcore::header::BlockNumber;
use ethcore::transaction::SignedTransaction; use ethcore::transaction::SignedTransaction;
use ethcore::receipt::{Receipt, RichReceipt}; use ethcore::receipt::{Receipt, RichReceipt};
use ethcore::miner::{MinerService, MinerStatus, TransactionImportResult}; use ethcore::miner::{MinerService, MinerStatus, TransactionImportResult, LocalTransactionStatus};
/// Test miner service. /// Test miner service.
pub struct TestMinerService { pub struct TestMinerService {
@ -34,6 +34,8 @@ pub struct TestMinerService {
pub latest_closed_block: Mutex<Option<ClosedBlock>>, pub latest_closed_block: Mutex<Option<ClosedBlock>>,
/// Pre-existed pending transactions /// Pre-existed pending transactions
pub pending_transactions: Mutex<HashMap<H256, SignedTransaction>>, pub pending_transactions: Mutex<HashMap<H256, SignedTransaction>>,
/// Pre-existed local transactions
pub local_transactions: Mutex<BTreeMap<H256, LocalTransactionStatus>>,
/// Pre-existed pending receipts /// Pre-existed pending receipts
pub pending_receipts: Mutex<BTreeMap<H256, Receipt>>, pub pending_receipts: Mutex<BTreeMap<H256, Receipt>>,
/// Last nonces. /// Last nonces.
@ -53,6 +55,7 @@ impl Default for TestMinerService {
imported_transactions: Mutex::new(Vec::new()), imported_transactions: Mutex::new(Vec::new()),
latest_closed_block: Mutex::new(None), latest_closed_block: Mutex::new(None),
pending_transactions: Mutex::new(HashMap::new()), pending_transactions: Mutex::new(HashMap::new()),
local_transactions: Mutex::new(BTreeMap::new()),
pending_receipts: Mutex::new(BTreeMap::new()), pending_receipts: Mutex::new(BTreeMap::new()),
last_nonces: RwLock::new(HashMap::new()), last_nonces: RwLock::new(HashMap::new()),
min_gas_price: RwLock::new(U256::from(20_000_000)), min_gas_price: RwLock::new(U256::from(20_000_000)),
@ -195,6 +198,10 @@ impl MinerService for TestMinerService {
self.pending_transactions.lock().values().cloned().collect() self.pending_transactions.lock().values().cloned().collect()
} }
fn local_transactions(&self) -> BTreeMap<H256, LocalTransactionStatus> {
self.local_transactions.lock().iter().map(|(hash, stats)| (*hash, stats.clone())).collect()
}
fn pending_transactions(&self, _best_block: BlockNumber) -> Vec<SignedTransaction> { fn pending_transactions(&self, _best_block: BlockNumber) -> Vec<SignedTransaction> {
self.pending_transactions.lock().values().cloned().collect() self.pending_transactions.lock().values().cloned().collect()
} }

View File

@ -16,8 +16,9 @@
//! Test implementation of SyncProvider. //! Test implementation of SyncProvider.
use util::{RwLock}; use std::collections::BTreeMap;
use ethsync::{SyncProvider, SyncStatus, SyncState, PeerInfo}; use util::{H256, RwLock};
use ethsync::{SyncProvider, SyncStatus, SyncState, PeerInfo, TransactionStats};
/// TestSyncProvider config. /// TestSyncProvider config.
pub struct Config { pub struct Config {
@ -97,5 +98,22 @@ impl SyncProvider for TestSyncProvider {
fn enode(&self) -> Option<String> { fn enode(&self) -> Option<String> {
None None
} }
fn transactions_stats(&self) -> BTreeMap<H256, TransactionStats> {
map![
1.into() => TransactionStats {
first_seen: 10,
propagated_to: map![
128.into() => 16
]
},
5.into() => TransactionStats {
first_seen: 16,
propagated_to: map![
16.into() => 1
]
}
]
}
} }

View File

@ -18,8 +18,10 @@ use std::str::FromStr;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use rustc_serialize::hex::ToHex;
use time::get_time;
use rlp; use rlp;
use jsonrpc_core::IoHandler;
use util::{Uint, U256, Address, H256, FixedHash, Mutex}; use util::{Uint, U256, Address, H256, FixedHash, Mutex};
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use ethcore::client::{TestBlockChainClient, EachBlockWith, Executed, TransactionID}; use ethcore::client::{TestBlockChainClient, EachBlockWith, Executed, TransactionID};
@ -28,10 +30,10 @@ use ethcore::receipt::LocalizedReceipt;
use ethcore::transaction::{Transaction, Action}; use ethcore::transaction::{Transaction, Action};
use ethcore::miner::{ExternalMiner, MinerService}; use ethcore::miner::{ExternalMiner, MinerService};
use ethsync::SyncState; use ethsync::SyncState;
use jsonrpc_core::IoHandler;
use v1::{Eth, EthClient, EthClientOptions, EthFilter, EthFilterClient, EthSigning, SigningUnsafeClient}; use v1::{Eth, EthClient, EthClientOptions, EthFilter, EthFilterClient, EthSigning, SigningUnsafeClient};
use v1::tests::helpers::{TestSyncProvider, Config, TestMinerService, TestSnapshotService}; use v1::tests::helpers::{TestSyncProvider, Config, TestMinerService, TestSnapshotService};
use rustc_serialize::hex::ToHex;
use time::get_time;
fn blockchain_client() -> Arc<TestBlockChainClient> { fn blockchain_client() -> Arc<TestBlockChainClient> {
let client = TestBlockChainClient::new(); let client = TestBlockChainClient::new();
@ -798,9 +800,25 @@ fn rpc_eth_sign_transaction() {
}; };
let signature = tester.accounts_provider.sign(address, None, t.hash(None)).unwrap(); let signature = tester.accounts_provider.sign(address, None, t.hash(None)).unwrap();
let t = t.with_signature(signature, None); let t = t.with_signature(signature, None);
let signature = t.signature();
let rlp = rlp::encode(&t); let rlp = rlp::encode(&t);
let response = r#"{"jsonrpc":"2.0","result":"0x"#.to_owned() + &rlp.to_hex() + r#"","id":1}"#; 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,"# +
&format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) +
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) +
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
&format!("\"v\":{},", signature.v()) +
r#""value":"0x9184e72a""# +
r#"}},"id":1}"#;
tester.miner.last_nonces.write().insert(address.clone(), U256::zero()); tester.miner.last_nonces.write().insert(address.clone(), U256::zero());

View File

@ -18,8 +18,9 @@ use std::sync::Arc;
use util::log::RotatingLogger; use util::log::RotatingLogger;
use util::Address; use util::Address;
use ethsync::ManageNetwork; use ethsync::ManageNetwork;
use ethcore::client::{TestBlockChainClient};
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use ethcore::client::{TestBlockChainClient};
use ethcore::miner::LocalTransactionStatus;
use ethstore::ethkey::{Generator, Random}; use ethstore::ethkey::{Generator, Random};
use jsonrpc_core::IoHandler; use jsonrpc_core::IoHandler;
@ -355,3 +356,28 @@ fn rpc_parity_next_nonce() {
assert_eq!(io1.handle_request_sync(&request), Some(response1.to_owned())); assert_eq!(io1.handle_request_sync(&request), Some(response1.to_owned()));
assert_eq!(io2.handle_request_sync(&request), Some(response2.to_owned())); assert_eq!(io2.handle_request_sync(&request), Some(response2.to_owned()));
} }
#[test]
fn rpc_parity_transactions_stats() {
let deps = Dependencies::new();
let io = deps.default_client();
let request = r#"{"jsonrpc": "2.0", "method": "parity_pendingTransactionsStats", "params":[], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":{"0x0000000000000000000000000000000000000000000000000000000000000001":{"firstSeen":10,"propagatedTo":{"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080":16}},"0x0000000000000000000000000000000000000000000000000000000000000005":{"firstSeen":16,"propagatedTo":{"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010":1}}},"id":1}"#;
assert_eq!(io.handle_request_sync(request), Some(response.to_owned()));
}
#[test]
fn rpc_parity_local_transactions() {
let deps = Dependencies::new();
let io = deps.default_client();
deps.miner.local_transactions.lock().insert(10.into(), LocalTransactionStatus::Pending);
deps.miner.local_transactions.lock().insert(15.into(), LocalTransactionStatus::Future);
let request = r#"{"jsonrpc": "2.0", "method": "parity_localTransactions", "params":[], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":{"0x000000000000000000000000000000000000000000000000000000000000000a":{"status":"pending"},"0x000000000000000000000000000000000000000000000000000000000000000f":{"status":"future"}},"id":1}"#;
assert_eq!(io.handle_request_sync(request), Some(response.to_owned()));
}

View File

@ -112,7 +112,7 @@ fn should_be_able_to_set_meta() {
let request = r#"{"jsonrpc": "2.0", "method": "parity_accountsInfo", "params": [], "id": 1}"#; let request = r#"{"jsonrpc": "2.0", "method": "parity_accountsInfo", "params": [], "id": 1}"#;
let res = tester.io.handle_request_sync(request); let res = tester.io.handle_request_sync(request);
let response = format!("{{\"jsonrpc\":\"2.0\",\"result\":{{\"0x{}\":{{\"meta\":\"{{foo: 69}}\",\"name\":\"{}\",\"uuid\":\"{}\"}}}},\"id\":1}}", address.hex(), uuid, uuid); let response = format!("{{\"jsonrpc\":\"2.0\",\"result\":{{\"0x{}\":{{\"meta\":\"{{foo: 69}}\",\"name\":\"\",\"uuid\":\"{}\"}}}},\"id\":1}}", address.hex(), uuid);
assert_eq!(res, Some(response)); assert_eq!(res, Some(response));
} }

View File

@ -268,16 +268,32 @@ fn should_add_sign_transaction_to_the_queue() {
}; };
let signature = tester.accounts.sign(address, Some("test".into()), t.hash(None)).unwrap(); let signature = tester.accounts.sign(address, Some("test".into()), t.hash(None)).unwrap();
let t = t.with_signature(signature, None); let t = t.with_signature(signature, None);
let signature = t.signature();
let rlp = rlp::encode(&t); let rlp = rlp::encode(&t);
let response = r#"{"jsonrpc":"2.0","result":"0x"#.to_owned() + &rlp.to_hex() + r#"","id":1}"#; 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,"# +
&format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","nonce":"0x1","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
&format!("\"r\":\"0x{}\",", signature.r().to_hex()) +
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
&format!("\"s\":\"0x{}\",", signature.s().to_hex()) +
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
&format!("\"v\":{},", signature.v()) +
r#""value":"0x9184e72a""# +
r#"}},"id":1}"#;
// then // then
tester.miner.last_nonces.write().insert(address.clone(), U256::zero()); tester.miner.last_nonces.write().insert(address.clone(), U256::zero());
let async_result = tester.io.handle_request(&request).unwrap(); let async_result = tester.io.handle_request(&request).unwrap();
assert_eq!(tester.signer.requests().len(), 1); assert_eq!(tester.signer.requests().len(), 1);
// respond // respond
tester.signer.request_confirmed(1.into(), Ok(ConfirmationResponse::SignTransaction(rlp.to_vec().into()))); tester.signer.request_confirmed(1.into(), Ok(ConfirmationResponse::SignTransaction(t.into())));
assert!(async_result.on_result(move |res| { assert!(async_result.on_result(move |res| {
assert_eq!(res, response.to_owned()); assert_eq!(res, response.to_owned());
})); }));

View File

@ -102,6 +102,10 @@ build_rpc_trait! {
#[rpc(name = "eth_sendRawTransaction")] #[rpc(name = "eth_sendRawTransaction")]
fn send_raw_transaction(&self, Bytes) -> Result<H256, Error>; fn send_raw_transaction(&self, Bytes) -> Result<H256, Error>;
/// Alias of `eth_sendRawTransaction`.
#[rpc(name = "eth_submitTransaction")]
fn submit_transaction(&self, Bytes) -> Result<H256, Error>;
/// Call contract, returning the output data. /// Call contract, returning the output data.
#[rpc(name = "eth_call")] #[rpc(name = "eth_call")]
fn call(&self, CallRequest, Trailing<BlockNumber>) -> Result<Bytes, Error>; fn call(&self, CallRequest, Trailing<BlockNumber>) -> Result<Bytes, Error>;

View File

@ -17,7 +17,7 @@
//! Eth rpc interface. //! Eth rpc interface.
use v1::helpers::auto_args::{WrapAsync, Ready}; use v1::helpers::auto_args::{WrapAsync, Ready};
use v1::types::{H160, H256, H520, TransactionRequest, Bytes}; use v1::types::{H160, H256, H520, TransactionRequest, RichRawTransaction};
build_rpc_trait! { build_rpc_trait! {
/// Signing methods implementation relying on unlocked accounts. /// Signing methods implementation relying on unlocked accounts.
@ -33,9 +33,9 @@ build_rpc_trait! {
fn send_transaction(&self, Ready<H256>, TransactionRequest); fn send_transaction(&self, Ready<H256>, TransactionRequest);
/// Signs transactions without dispatching it to the network. /// Signs transactions without dispatching it to the network.
/// Returns signed transaction RLP representation. /// Returns signed transaction RLP representation and the transaction itself.
/// It can be later submitted using `eth_sendRawTransaction`. /// It can be later submitted using `eth_sendRawTransaction/eth_submitTransaction`.
#[rpc(async, name = "eth_signTransaction")] #[rpc(async, name = "eth_signTransaction")]
fn sign_transaction(&self, Ready<Bytes>, TransactionRequest); fn sign_transaction(&self, Ready<RichRawTransaction>, TransactionRequest);
} }
} }

View File

@ -19,7 +19,11 @@ use jsonrpc_core::Error;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use v1::helpers::auto_args::Wrap; use v1::helpers::auto_args::Wrap;
use v1::types::{H160, H256, H512, U256, Bytes, Peers, Transaction, RpcSettings, Histogram}; use v1::types::{
H160, H256, H512, U256, Bytes,
Peers, Transaction, RpcSettings, Histogram,
TransactionStats, LocalTransactionStatus,
};
build_rpc_trait! { build_rpc_trait! {
/// Parity-specific rpc interface. /// Parity-specific rpc interface.
@ -115,6 +119,14 @@ build_rpc_trait! {
#[rpc(name = "parity_pendingTransactions")] #[rpc(name = "parity_pendingTransactions")]
fn pending_transactions(&self) -> Result<Vec<Transaction>, Error>; fn pending_transactions(&self) -> Result<Vec<Transaction>, Error>;
/// Returns propagation statistics on transactions pending in the queue.
#[rpc(name = "parity_pendingTransactionsStats")]
fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error>;
/// Returns a list of current and past local transactions with status details.
#[rpc(name = "parity_localTransactions")]
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error>;
/// Returns current Trusted Signer port or an error if signer is disabled. /// Returns current Trusted Signer port or an error if signer is disabled.
#[rpc(name = "parity_signerPort")] #[rpc(name = "parity_signerPort")]
fn signer_port(&self) -> Result<u16, Error>; fn signer_port(&self) -> Result<u16, Error>;

View File

@ -18,7 +18,7 @@
use std::fmt; use std::fmt;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use v1::types::{U256, TransactionRequest, H160, H256, H520, Bytes}; use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes};
use v1::helpers; use v1::helpers;
/// Confirmation waiting in a queue /// Confirmation waiting in a queue
@ -76,12 +76,12 @@ impl From<(H160, Bytes)> for DecryptRequest {
} }
/// Confirmation response for particular payload /// Confirmation response for particular payload
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq)]
pub enum ConfirmationResponse { pub enum ConfirmationResponse {
/// Transaction Hash /// Transaction Hash
SendTransaction(H256), SendTransaction(H256),
/// Transaction RLP /// Transaction RLP
SignTransaction(Bytes), SignTransaction(RichRawTransaction),
/// Signature /// Signature
Signature(H520), Signature(H520),
/// Decrypted data /// Decrypted data

View File

@ -43,8 +43,8 @@ pub use self::filter::{Filter, FilterChanges};
pub use self::hash::{H64, H160, H256, H512, H520, H2048}; pub use self::hash::{H64, H160, H256, H512, H520, H2048};
pub use self::index::Index; pub use self::index::Index;
pub use self::log::Log; pub use self::log::Log;
pub use self::sync::{SyncStatus, SyncInfo, Peers, PeerInfo, PeerNetworkInfo, PeerProtocolsInfo, PeerEthereumProtocolInfo}; pub use self::sync::{SyncStatus, SyncInfo, Peers, PeerInfo, PeerNetworkInfo, PeerProtocolsInfo, PeerEthereumProtocolInfo, TransactionStats};
pub use self::transaction::Transaction; pub use self::transaction::{Transaction, RichRawTransaction, LocalTransactionStatus};
pub use self::transaction_request::TransactionRequest; pub use self::transaction_request::TransactionRequest;
pub use self::receipt::Receipt; pub use self::receipt::Receipt;
pub use self::rpc_settings::RpcSettings; pub use self::rpc_settings::RpcSettings;

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use ethsync::PeerInfo as SyncPeerInfo; use std::collections::BTreeMap;
use ethsync::{PeerInfo as SyncPeerInfo, TransactionStats as SyncTransactionStats};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use v1::types::U256; use v1::types::{U256, H512};
/// Sync info /// Sync info
#[derive(Default, Debug, Serialize, PartialEq)] #[derive(Default, Debug, Serialize, PartialEq)]
@ -117,8 +118,19 @@ impl Serialize for SyncStatus {
} }
} }
/// Propagation statistics for pending transaction.
#[derive(Default, Debug, Serialize)]
pub struct TransactionStats {
/// Block no this transaction was first seen.
#[serde(rename="firstSeen")]
pub first_seen: u64,
/// Peers this transaction was propagated to with count.
#[serde(rename="propagatedTo")]
pub propagated_to: BTreeMap<H512, usize>,
}
impl From<SyncPeerInfo> for PeerInfo { impl From<SyncPeerInfo> for PeerInfo {
fn from(p: SyncPeerInfo) -> PeerInfo { fn from(p: SyncPeerInfo) -> Self {
PeerInfo { PeerInfo {
id: p.id, id: p.id,
name: p.client_version, name: p.client_version,
@ -138,10 +150,23 @@ impl From<SyncPeerInfo> for PeerInfo {
} }
} }
impl From<SyncTransactionStats> for TransactionStats {
fn from(s: SyncTransactionStats) -> Self {
TransactionStats {
first_seen: s.first_seen,
propagated_to: s.propagated_to
.into_iter()
.map(|(id, count)| (id.into(), count))
.collect()
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json; use serde_json;
use super::{SyncInfo, SyncStatus, Peers}; use std::collections::BTreeMap;
use super::{SyncInfo, SyncStatus, Peers, TransactionStats};
#[test] #[test]
fn test_serialize_sync_info() { fn test_serialize_sync_info() {
@ -176,4 +201,17 @@ mod tests {
let serialized = serde_json::to_string(&t).unwrap(); let serialized = serde_json::to_string(&t).unwrap();
assert_eq!(serialized, r#"{"startingBlock":"0x0","currentBlock":"0x0","highestBlock":"0x0","warpChunksAmount":null,"warpChunksProcessed":null,"blockGap":["0x1","0x5"]}"#) assert_eq!(serialized, r#"{"startingBlock":"0x0","currentBlock":"0x0","highestBlock":"0x0","warpChunksAmount":null,"warpChunksProcessed":null,"blockGap":["0x1","0x5"]}"#)
} }
#[test]
fn test_serialize_transaction_stats() {
let stats = TransactionStats {
first_seen: 100,
propagated_to: map![
10.into() => 50
]
};
let serialized = serde_json::to_string(&stats).unwrap();
assert_eq!(serialized, r#"{"firstSeen":100,"propagatedTo":{"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a":50}}"#)
}
} }

Some files were not shown because too many files have changed in this diff Show More