Merge remote-tracking branch 'origin/master' into check-updates

This commit is contained in:
Gav Wood 2016-12-09 20:45:15 +01:00
commit 46af3d18da
No known key found for this signature in database
GPG Key ID: C49C1ACA1CC9B252
144 changed files with 2718 additions and 1165 deletions

View File

@ -401,7 +401,7 @@ test-darwin:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- ./test.sh $CARGOFLAGS --no-release - ./test.sh $CARGOFLAGS
tags: tags:
- osx - osx
allow_failure: true allow_failure: true
@ -422,13 +422,14 @@ test-rust-stable:
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi - echo "rust/js modified: $RUST_FILES_MODIFIED / $JS_FILES_MODIFIED"
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- echo $JS_FILES_MODIFIED - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"&./test.sh $CARGOFLAGS --no-release; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi - if [ "$RUST_FILES_MODIFIED" = 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi
tags: tags:
- rust - rust
- rust-stable - rust-stable
@ -437,13 +438,13 @@ js-test:
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi
tags: tags:
- rust - rust
- rust-stable - rust-stable
@ -457,7 +458,7 @@ test-rust-beta:
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- ./test.sh $CARGOFLAGS --no-release - ./test.sh $CARGOFLAGS
tags: tags:
- rust - rust
- rust-beta - rust-beta
@ -471,7 +472,7 @@ test-rust-nightly:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- ./test.sh $CARGOFLAGS --no-release - ./test.sh $CARGOFLAGS
tags: tags:
- rust - rust
- rust-nightly - rust-nightly
@ -484,11 +485,11 @@ js-release:
- stable - stable
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/install-deps.sh;fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/build.sh&&./js/scripts/release.sh; fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi
tags: tags:
- javascript - javascript

View File

@ -16,7 +16,7 @@ git:
matrix: matrix:
include: include:
- rust: stable - rust: stable
env: RUN_TESTS="true" TEST_OPTIONS="--no-release" env: RUN_TESTS="true" TEST_OPTIONS=""
- rust: stable - rust: stable
env: RUN_COVERAGE="true" env: RUN_COVERAGE="true"
- rust: stable - rust: stable
@ -71,8 +71,7 @@ install:
script: script:
- if [ "$RUN_TESTS" = "true" ]; then - if [ "$RUN_TESTS" = "true" ]; then
./js/scripts/lint.sh && ./js/scripts/lint.sh &&
./js/scripts/test.sh && travis_wait 40 ./test.sh $TEST_OPTIONS;
./test.sh $TEST_OPTIONS --verbose;
fi fi
- if [ "$RUN_COVERAGE" = "true" ]; then ./scripts/cov.sh "$KCOV_CMD"; fi - if [ "$RUN_COVERAGE" = "true" ]; then ./scripts/cov.sh "$KCOV_CMD"; fi

90
Cargo.lock generated
View File

@ -10,18 +10,18 @@ dependencies = [
"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)",
"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.5.0",
"ethcore-hash-fetch 1.5.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.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-hypervisor 1.2.0", "ethcore-ipc-hypervisor 1.2.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"ethcore-ipc-tests 0.1.0", "ethcore-ipc-tests 0.1.0",
"ethcore-logger 1.5.0", "ethcore-logger 1.5.0",
"ethcore-rpc 1.5.0", "ethcore-rpc 1.5.0",
"ethcore-signer 1.5.0", "ethcore-signer 1.5.0",
"ethcore-stratum 1.4.0", "ethcore-stratum 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethsync 1.5.0", "ethsync 1.5.0",
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -273,7 +273,7 @@ dependencies = [
[[package]] [[package]]
name = "ethash" name = "ethash"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"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)",
"parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
@ -292,19 +292,19 @@ dependencies = [
"crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.2.9 (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)", "ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethash 1.4.0", "ethash 1.5.0",
"ethcore-bloom-journal 0.1.0", "ethcore-bloom-journal 0.1.0",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-hash-fetch 1.5.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.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethjson 0.1.0", "ethjson 0.1.0",
"ethkey 0.2.0", "ethkey 0.2.0",
"ethstore 0.1.0", "ethstore 0.1.0",
"evmjit 1.4.0", "evmjit 1.5.0",
"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.10.0-a.0 (git+https://github.com/ethcore/hyper)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -345,7 +345,7 @@ version = "1.5.0"
dependencies = [ dependencies = [
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-hash-fetch 1.5.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",
@ -358,7 +358,7 @@ dependencies = [
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-ui 1.4.0", "parity-ui 1.5.0",
"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)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -372,7 +372,7 @@ dependencies = [
[[package]] [[package]]
name = "ethcore-devtools" name = "ethcore-devtools"
version = "1.4.0" version = "1.5.0"
dependencies = [ 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)",
] ]
@ -402,9 +402,9 @@ dependencies = [
[[package]] [[package]]
name = "ethcore-ipc" name = "ethcore-ipc"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)",
"semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -412,7 +412,7 @@ dependencies = [
[[package]] [[package]]
name = "ethcore-ipc-codegen" name = "ethcore-ipc-codegen"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
"quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -425,9 +425,9 @@ dependencies = [
name = "ethcore-ipc-hypervisor" name = "ethcore-ipc-hypervisor"
version = "1.2.0" version = "1.2.0"
dependencies = [ dependencies = [
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"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)",
"nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)",
"semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -436,9 +436,9 @@ dependencies = [
[[package]] [[package]]
name = "ethcore-ipc-nano" name = "ethcore-ipc-nano"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"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)",
"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)",
"nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)",
@ -448,10 +448,10 @@ dependencies = [
name = "ethcore-ipc-tests" name = "ethcore-ipc-tests"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"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)",
"nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)", "nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)",
@ -477,7 +477,7 @@ version = "1.5.0"
dependencies = [ dependencies = [
"ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-io 1.5.0", "ethcore-io 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethcrypto 0.1.0", "ethcrypto 0.1.0",
@ -501,11 +501,11 @@ name = "ethcore-rpc"
version = "1.5.0" version = "1.5.0"
dependencies = [ dependencies = [
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"ethash 1.4.0", "ethash 1.5.0",
"ethcore 1.5.0", "ethcore 1.5.0",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-io 1.5.0", "ethcore-io 1.5.0",
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethcrypto 0.1.0", "ethcrypto 0.1.0",
"ethjson 0.1.0", "ethjson 0.1.0",
@ -532,14 +532,14 @@ version = "1.5.0"
dependencies = [ dependencies = [
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-io 1.5.0", "ethcore-io 1.5.0",
"ethcore-rpc 1.5.0", "ethcore-rpc 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"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)",
"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)",
"parity-ui 1.4.0", "parity-ui 1.5.0",
"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)",
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)", "ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)",
@ -547,13 +547,13 @@ dependencies = [
[[package]] [[package]]
name = "ethcore-stratum" name = "ethcore-stratum"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"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)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
@ -575,7 +575,7 @@ dependencies = [
"eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)", "eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)",
"ethcore-bigint 0.1.2", "ethcore-bigint 0.1.2",
"ethcore-bloom-journal 0.1.0", "ethcore-bloom-journal 0.1.0",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.5.0",
"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)",
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
@ -664,9 +664,9 @@ dependencies = [
"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)",
"ethcore 1.5.0", "ethcore 1.5.0",
"ethcore-io 1.5.0", "ethcore-io 1.5.0",
"ethcore-ipc 1.4.0", "ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.4.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.4.0", "ethcore-ipc-nano 1.5.0",
"ethcore-network 1.5.0", "ethcore-network 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"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)",
@ -680,7 +680,7 @@ dependencies = [
[[package]] [[package]]
name = "evmjit" name = "evmjit"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1257,7 +1257,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui" name = "parity-ui"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"parity-ui-dev 1.4.0", "parity-ui-dev 1.4.0",
"parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)", "parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)",
@ -1274,7 +1274,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#1690a80b9f56e33c8344f28eb637bacd97c9da14" source = "git+https://github.com/ethcore/js-precompiled.git#3d3b2f9e8e8b0fd62c172240bfd001a317cf2979"
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

@ -88,4 +88,4 @@ name = "parity"
[profile.release] [profile.release]
debug = false debug = false
lto = false lto = false
panic = "abort"

View File

@ -1,7 +1,7 @@
[package] [package]
description = "Base Package for all Parity built-in dapps" description = "Base Package for all Parity built-in dapps"
name = "parity-dapps-glue" name = "parity-dapps-glue"
version = "1.4.0" version = "1.5.0"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Ethcore <admin@ethcore.io"] authors = ["Ethcore <admin@ethcore.io"]
build = "build.rs" build = "build.rs"

View File

@ -3,7 +3,7 @@ description = "Ethcore Parity UI"
homepage = "http://ethcore.io" homepage = "http://ethcore.io"
license = "GPL-3.0" license = "GPL-3.0"
name = "parity-ui" name = "parity-ui"
version = "1.4.0" version = "1.5.0"
authors = ["Ethcore <admin@ethcore.io>"] authors = ["Ethcore <admin@ethcore.io>"]
[build-dependencies] [build-dependencies]

View File

@ -3,7 +3,7 @@ description = "Ethcore Database"
homepage = "http://ethcore.io" homepage = "http://ethcore.io"
license = "GPL-3.0" license = "GPL-3.0"
name = "ethcore-db" name = "ethcore-db"
version = "1.4.0" version = "1.5.0"
authors = ["Ethcore <admin@ethcore.io>"] authors = ["Ethcore <admin@ethcore.io>"]
build = "build.rs" build = "build.rs"

View File

@ -3,7 +3,7 @@ description = "Ethcore development/test/build tools"
homepage = "http://ethcore.io" homepage = "http://ethcore.io"
license = "GPL-3.0" license = "GPL-3.0"
name = "ethcore-devtools" name = "ethcore-devtools"
version = "1.4.0" version = "1.5.0"
authors = ["Ethcore <admin@ethcore.io>"] authors = ["Ethcore <admin@ethcore.io>"]
[dependencies] [dependencies]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ethash" name = "ethash"
version = "1.4.0" version = "1.5.0"
authors = ["arkpar <arkadiy@ethcore.io"] authors = ["arkpar <arkadiy@ethcore.io"]
[lib] [lib]

View File

@ -146,7 +146,7 @@ pub trait BlockProvider {
} }
#[derive(Debug, Hash, Eq, PartialEq, Clone)] #[derive(Debug, Hash, Eq, PartialEq, Clone)]
enum CacheID { enum CacheId {
BlockHeader(H256), BlockHeader(H256),
BlockBody(H256), BlockBody(H256),
BlockDetails(H256), BlockDetails(H256),
@ -160,7 +160,7 @@ impl bc::group::BloomGroupDatabase for BlockChain {
fn blooms_at(&self, position: &bc::group::GroupPosition) -> Option<bc::group::BloomGroup> { fn blooms_at(&self, position: &bc::group::GroupPosition) -> Option<bc::group::BloomGroup> {
let position = LogGroupPosition::from(position.clone()); let position = LogGroupPosition::from(position.clone());
let result = self.db.read_with_cache(db::COL_EXTRA, &self.blocks_blooms, &position).map(Into::into); let result = self.db.read_with_cache(db::COL_EXTRA, &self.blocks_blooms, &position).map(Into::into);
self.cache_man.lock().note_used(CacheID::BlocksBlooms(position)); self.cache_man.lock().note_used(CacheId::BlocksBlooms(position));
result result
} }
} }
@ -193,7 +193,7 @@ pub struct BlockChain {
db: Arc<Database>, db: Arc<Database>,
cache_man: Mutex<CacheManager<CacheID>>, cache_man: Mutex<CacheManager<CacheId>>,
pending_best_block: RwLock<Option<BestBlock>>, pending_best_block: RwLock<Option<BestBlock>>,
pending_block_hashes: RwLock<HashMap<BlockNumber, H256>>, pending_block_hashes: RwLock<HashMap<BlockNumber, H256>>,
@ -270,7 +270,7 @@ impl BlockProvider for BlockChain {
None => None None => None
}; };
self.cache_man.lock().note_used(CacheID::BlockHeader(hash.clone())); self.cache_man.lock().note_used(CacheId::BlockHeader(hash.clone()));
result result
} }
@ -306,7 +306,7 @@ impl BlockProvider for BlockChain {
None => None None => None
}; };
self.cache_man.lock().note_used(CacheID::BlockBody(hash.clone())); self.cache_man.lock().note_used(CacheId::BlockBody(hash.clone()));
result result
} }
@ -314,28 +314,28 @@ impl BlockProvider for BlockChain {
/// Get the familial details concerning a block. /// Get the familial details concerning a block.
fn block_details(&self, hash: &H256) -> Option<BlockDetails> { fn block_details(&self, hash: &H256) -> Option<BlockDetails> {
let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_details, hash); let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_details, hash);
self.cache_man.lock().note_used(CacheID::BlockDetails(hash.clone())); self.cache_man.lock().note_used(CacheId::BlockDetails(hash.clone()));
result result
} }
/// Get the hash of given block's number. /// Get the hash of given block's number.
fn block_hash(&self, index: BlockNumber) -> Option<H256> { fn block_hash(&self, index: BlockNumber) -> Option<H256> {
let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_hashes, &index); let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_hashes, &index);
self.cache_man.lock().note_used(CacheID::BlockHashes(index)); self.cache_man.lock().note_used(CacheId::BlockHashes(index));
result result
} }
/// Get the address of transaction with given hash. /// Get the address of transaction with given hash.
fn transaction_address(&self, hash: &H256) -> Option<TransactionAddress> { fn transaction_address(&self, hash: &H256) -> Option<TransactionAddress> {
let result = self.db.read_with_cache(db::COL_EXTRA, &self.transaction_addresses, hash); let result = self.db.read_with_cache(db::COL_EXTRA, &self.transaction_addresses, hash);
self.cache_man.lock().note_used(CacheID::TransactionAddresses(hash.clone())); self.cache_man.lock().note_used(CacheId::TransactionAddresses(hash.clone()));
result result
} }
/// Get receipts of block with given hash. /// Get receipts of block with given hash.
fn block_receipts(&self, hash: &H256) -> Option<BlockReceipts> { fn block_receipts(&self, hash: &H256) -> Option<BlockReceipts> {
let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_receipts, hash); let result = self.db.read_with_cache(db::COL_EXTRA, &self.block_receipts, hash);
self.cache_man.lock().note_used(CacheID::BlockReceipts(hash.clone())); self.cache_man.lock().note_used(CacheId::BlockReceipts(hash.clone()));
result result
} }
@ -809,7 +809,7 @@ impl BlockChain {
let mut write_details = self.block_details.write(); let mut write_details = self.block_details.write();
batch.extend_with_cache(db::COL_EXTRA, &mut *write_details, update, CacheUpdatePolicy::Overwrite); batch.extend_with_cache(db::COL_EXTRA, &mut *write_details, update, CacheUpdatePolicy::Overwrite);
self.cache_man.lock().note_used(CacheID::BlockDetails(block_hash)); self.cache_man.lock().note_used(CacheId::BlockDetails(block_hash));
} }
#[cfg_attr(feature="dev", allow(similar_names))] #[cfg_attr(feature="dev", allow(similar_names))]
@ -968,15 +968,15 @@ impl BlockChain {
let mut cache_man = self.cache_man.lock(); let mut cache_man = self.cache_man.lock();
for n in pending_hashes_keys { for n in pending_hashes_keys {
cache_man.note_used(CacheID::BlockHashes(n)); cache_man.note_used(CacheId::BlockHashes(n));
} }
for hash in enacted_txs_keys { for hash in enacted_txs_keys {
cache_man.note_used(CacheID::TransactionAddresses(hash)); cache_man.note_used(CacheId::TransactionAddresses(hash));
} }
for hash in pending_block_hashes { for hash in pending_block_hashes {
cache_man.note_used(CacheID::BlockDetails(hash)); cache_man.note_used(CacheId::BlockDetails(hash));
} }
} }
@ -1244,13 +1244,13 @@ impl BlockChain {
cache_man.collect_garbage(current_size, | ids | { cache_man.collect_garbage(current_size, | ids | {
for id in &ids { for id in &ids {
match *id { match *id {
CacheID::BlockHeader(ref h) => { block_headers.remove(h); }, CacheId::BlockHeader(ref h) => { block_headers.remove(h); },
CacheID::BlockBody(ref h) => { block_bodies.remove(h); }, CacheId::BlockBody(ref h) => { block_bodies.remove(h); },
CacheID::BlockDetails(ref h) => { block_details.remove(h); } CacheId::BlockDetails(ref h) => { block_details.remove(h); }
CacheID::BlockHashes(ref h) => { block_hashes.remove(h); } CacheId::BlockHashes(ref h) => { block_hashes.remove(h); }
CacheID::TransactionAddresses(ref h) => { transaction_addresses.remove(h); } CacheId::TransactionAddresses(ref h) => { transaction_addresses.remove(h); }
CacheID::BlocksBlooms(ref h) => { blocks_blooms.remove(h); } CacheId::BlocksBlooms(ref h) => { blocks_blooms.remove(h); }
CacheID::BlockReceipts(ref h) => { block_receipts.remove(h); } CacheId::BlockReceipts(ref h) => { block_receipts.remove(h); }
} }
} }

View File

@ -1130,6 +1130,10 @@ impl BlockChainClient for Client {
self.transaction_address(id).and_then(|address| self.chain.read().transaction(&address)) self.transaction_address(id).and_then(|address| self.chain.read().transaction(&address))
} }
fn transaction_block(&self, id: TransactionId) -> Option<H256> {
self.transaction_address(id).map(|addr| addr.block_hash)
}
fn uncle(&self, id: UncleId) -> Option<Bytes> { fn uncle(&self, id: UncleId) -> Option<Bytes> {
let index = id.position; let index = id.position;
self.block_body(id.block).and_then(|body| BodyView::new(&body).uncle_rlp_at(index)) self.block_body(id.block).and_then(|body| BodyView::new(&body).uncle_rlp_at(index))

View File

@ -432,6 +432,10 @@ impl BlockChainClient for TestBlockChainClient {
None // Simple default. None // Simple default.
} }
fn transaction_block(&self, _id: TransactionId) -> Option<H256> {
None // Simple default.
}
fn uncle(&self, _id: UncleId) -> Option<Bytes> { fn uncle(&self, _id: UncleId) -> Option<Bytes> {
None // Simple default. None // Simple default.
} }

View File

@ -129,6 +129,9 @@ pub trait BlockChainClient : Sync + Send {
/// Get transaction with given hash. /// Get transaction with given hash.
fn transaction(&self, id: TransactionId) -> Option<LocalizedTransaction>; fn transaction(&self, id: TransactionId) -> Option<LocalizedTransaction>;
/// Get the hash of block that contains the transaction, if any.
fn transaction_block(&self, id: TransactionId) -> Option<H256>;
/// Get uncle with given id. /// Get uncle with given id.
fn uncle(&self, id: UncleId) -> Option<Bytes>; fn uncle(&self, id: UncleId) -> Option<Bytes>;

View File

@ -21,13 +21,15 @@ use std::sync::Weak;
use std::time::{UNIX_EPOCH, Duration}; use std::time::{UNIX_EPOCH, Duration};
use util::*; use util::*;
use ethkey::{verify_address, Signature}; use ethkey::{verify_address, Signature};
use rlp::{Rlp, UntrustedRlp, View, encode}; use rlp::{UntrustedRlp, Rlp, View, encode};
use account_provider::AccountProvider; use account_provider::AccountProvider;
use block::*; use block::*;
use spec::CommonParams; use spec::CommonParams;
use engines::Engine; use engines::Engine;
use header::Header; use header::Header;
use error::{Error, BlockError}; use error::{Error, BlockError};
use blockchain::extras::BlockDetails;
use views::HeaderView;
use evm::Schedule; use evm::Schedule;
use ethjson; use ethjson;
use io::{IoContext, IoHandler, TimerToken, IoService, IoChannel}; use io::{IoContext, IoHandler, TimerToken, IoService, IoChannel};
@ -35,8 +37,6 @@ use service::ClientIoMessage;
use transaction::SignedTransaction; use transaction::SignedTransaction;
use env_info::EnvInfo; use env_info::EnvInfo;
use builtin::Builtin; use builtin::Builtin;
use blockchain::extras::BlockDetails;
use views::HeaderView;
/// `AuthorityRound` params. /// `AuthorityRound` params.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -68,18 +68,20 @@ pub struct AuthorityRound {
params: CommonParams, params: CommonParams,
our_params: AuthorityRoundParams, our_params: AuthorityRoundParams,
builtins: BTreeMap<Address, Builtin>, builtins: BTreeMap<Address, Builtin>,
transition_service: IoService<BlockArrived>, transition_service: IoService<()>,
message_channel: Mutex<Option<IoChannel<ClientIoMessage>>>, message_channel: Mutex<Option<IoChannel<ClientIoMessage>>>,
step: AtomicUsize, step: AtomicUsize,
proposed: AtomicBool, proposed: AtomicBool,
account_provider: Mutex<Option<Arc<AccountProvider>>>,
password: RwLock<Option<String>>,
} }
fn header_step(header: &Header) -> Result<usize, ::rlp::DecoderError> { fn header_step(header: &Header) -> Result<usize, ::rlp::DecoderError> {
UntrustedRlp::new(&header.seal()[0]).as_val() UntrustedRlp::new(&header.seal().get(0).expect("was either checked with verify_block_basic or is genesis; has 2 fields; qed (Make sure the spec file has a correct genesis seal)")).as_val()
} }
fn header_signature(header: &Header) -> Result<Signature, ::rlp::DecoderError> { fn header_signature(header: &Header) -> Result<Signature, ::rlp::DecoderError> {
UntrustedRlp::new(&header.seal()[1]).as_val::<H520>().map(Into::into) UntrustedRlp::new(&header.seal().get(1).expect("was checked with verify_block_basic; has 2 fields; qed")).as_val::<H520>().map(Into::into)
} }
trait AsMillis { trait AsMillis {
@ -101,10 +103,12 @@ impl AuthorityRound {
params: params, params: params,
our_params: our_params, our_params: our_params,
builtins: builtins, builtins: builtins,
transition_service: try!(IoService::<BlockArrived>::start()), transition_service: try!(IoService::<()>::start()),
message_channel: Mutex::new(None), message_channel: Mutex::new(None),
step: AtomicUsize::new(initial_step), step: AtomicUsize::new(initial_step),
proposed: AtomicBool::new(false) proposed: AtomicBool::new(false),
account_provider: Mutex::new(None),
password: RwLock::new(None),
}); });
let handler = TransitionHandler { engine: Arc::downgrade(&engine) }; let handler = TransitionHandler { engine: Arc::downgrade(&engine) };
try!(engine.transition_service.register_handler(Arc::new(handler))); try!(engine.transition_service.register_handler(Arc::new(handler)));
@ -143,20 +147,17 @@ struct TransitionHandler {
engine: Weak<AuthorityRound>, engine: Weak<AuthorityRound>,
} }
#[derive(Clone)]
struct BlockArrived;
const ENGINE_TIMEOUT_TOKEN: TimerToken = 23; const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
impl IoHandler<BlockArrived> for TransitionHandler { impl IoHandler<()> for TransitionHandler {
fn initialize(&self, io: &IoContext<BlockArrived>) { fn initialize(&self, io: &IoContext<()>) {
if let Some(engine) = self.engine.upgrade() { if let Some(engine) = self.engine.upgrade() {
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis()) io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
.unwrap_or_else(|e| warn!(target: "poa", "Failed to start consensus step timer: {}.", e)) .unwrap_or_else(|e| warn!(target: "poa", "Failed to start consensus step timer: {}.", e))
} }
} }
fn timeout(&self, io: &IoContext<BlockArrived>, timer: TimerToken) { fn timeout(&self, io: &IoContext<()>, timer: TimerToken) {
if timer == ENGINE_TIMEOUT_TOKEN { if timer == ENGINE_TIMEOUT_TOKEN {
if let Some(engine) = self.engine.upgrade() { if let Some(engine) = self.engine.upgrade() {
engine.step.fetch_add(1, AtomicOrdering::SeqCst); engine.step.fetch_add(1, AtomicOrdering::SeqCst);
@ -208,10 +209,6 @@ impl Engine for AuthorityRound {
}); });
} }
/// Apply the block reward on finalisation of the block.
/// This assumes that all uncles are valid uncles (i.e. of at least one generation before the current).
fn on_close_block(&self, _block: &mut ExecutedBlock) {}
fn is_sealer(&self, author: &Address) -> Option<bool> { fn is_sealer(&self, author: &Address) -> Option<bool> {
let p = &self.our_params; let p = &self.our_params;
Some(p.authorities.contains(author)) Some(p.authorities.contains(author))
@ -221,14 +218,14 @@ impl Engine for AuthorityRound {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will /// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned. /// be returned.
fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> {
if self.proposed.load(AtomicOrdering::SeqCst) { return None; } if self.proposed.load(AtomicOrdering::SeqCst) { return None; }
let header = block.header(); let header = block.header();
let step = self.step(); let step = self.step();
if self.is_step_proposer(step, header.author()) { if self.is_step_proposer(step, header.author()) {
if let Some(ap) = accounts { if let Some(ref ap) = *self.account_provider.lock() {
// Account should be permanently unlocked, otherwise sealing will fail. // Account should be permanently unlocked, otherwise sealing will fail.
if let Ok(signature) = ap.sign(*header.author(), None, header.bare_hash()) { if let Ok(signature) = ap.sign(*header.author(), self.password.read().clone(), header.bare_hash()) {
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step); trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
self.proposed.store(true, AtomicOrdering::SeqCst); self.proposed.store(true, AtomicOrdering::SeqCst);
return Some(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); return Some(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
@ -303,23 +300,30 @@ impl Engine for AuthorityRound {
t.sender().map(|_|()) // Perform EC recovery and cache sender t.sender().map(|_|()) // Perform EC recovery and cache sender
} }
fn register_message_channel(&self, message_channel: IoChannel<ClientIoMessage>) {
let mut guard = self.message_channel.lock();
*guard = Some(message_channel);
}
fn is_new_best_block(&self, _best_total_difficulty: U256, best_header: HeaderView, _parent_details: &BlockDetails, new_header: &HeaderView) -> bool { fn is_new_best_block(&self, _best_total_difficulty: U256, best_header: HeaderView, _parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
let new_number = new_header.number(); let new_number = new_header.number();
let best_number = best_header.number(); let best_number = best_header.number();
if new_number != best_number { if new_number != best_number {
new_number > best_number new_number > best_number
} else { } else {
// Take the oldest step at given height. // Take the oldest step at given height.
let new_step: usize = Rlp::new(&new_header.seal()[0]).as_val(); let new_step: usize = Rlp::new(&new_header.seal()[0]).as_val();
let best_step: usize = Rlp::new(&best_header.seal()[0]).as_val(); let best_step: usize = Rlp::new(&best_header.seal()[0]).as_val();
new_step < best_step new_step < best_step
} }
} }
fn register_message_channel(&self, message_channel: IoChannel<ClientIoMessage>) {
*self.message_channel.lock() = Some(message_channel);
}
fn set_signer(&self, _address: Address, password: String) {
*self.password.write() = Some(password);
}
fn register_account_provider(&self, account_provider: Arc<AccountProvider>) {
*self.account_provider.lock() = Some(account_provider);
}
} }
#[cfg(test)] #[cfg(test)]
@ -387,12 +391,11 @@ mod tests {
fn generates_seal_and_does_not_double_propose() { fn generates_seal_and_does_not_double_propose() {
let tap = AccountProvider::transient_provider(); let tap = AccountProvider::transient_provider();
let addr1 = tap.insert_account("1".sha3(), "1").unwrap(); let addr1 = tap.insert_account("1".sha3(), "1").unwrap();
tap.unlock_account_permanently(addr1, "1".into()).unwrap();
let addr2 = tap.insert_account("2".sha3(), "2").unwrap(); let addr2 = tap.insert_account("2".sha3(), "2").unwrap();
tap.unlock_account_permanently(addr2, "2".into()).unwrap();
let spec = Spec::new_test_round(); let spec = Spec::new_test_round();
let engine = &*spec.engine; let engine = &*spec.engine;
engine.register_account_provider(Arc::new(tap));
let genesis_header = spec.genesis_header(); let genesis_header = spec.genesis_header();
let mut db1 = get_temp_state_db().take(); let mut db1 = get_temp_state_db().take();
spec.ensure_db_good(&mut db1, &TrieFactory::new(TrieSpec::Secure)).unwrap(); spec.ensure_db_good(&mut db1, &TrieFactory::new(TrieSpec::Secure)).unwrap();
@ -404,16 +407,18 @@ mod tests {
let b2 = OpenBlock::new(engine, Default::default(), false, db2, &genesis_header, last_hashes, addr2, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b2 = OpenBlock::new(engine, Default::default(), false, db2, &genesis_header, last_hashes, addr2, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b2 = b2.close_and_lock(); let b2 = b2.close_and_lock();
if let Some(seal) = engine.generate_seal(b1.block(), Some(&tap)) { engine.set_signer(addr1, "1".into());
if let Some(seal) = engine.generate_seal(b1.block()) {
assert!(b1.clone().try_seal(engine, seal).is_ok()); assert!(b1.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden. // Second proposal is forbidden.
assert!(engine.generate_seal(b1.block(), Some(&tap)).is_none()); assert!(engine.generate_seal(b1.block()).is_none());
} }
if let Some(seal) = engine.generate_seal(b2.block(), Some(&tap)) { engine.set_signer(addr2, "2".into());
if let Some(seal) = engine.generate_seal(b2.block()) {
assert!(b2.clone().try_seal(engine, seal).is_ok()); assert!(b2.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden. // Second proposal is forbidden.
assert!(engine.generate_seal(b2.block(), Some(&tap)).is_none()); assert!(engine.generate_seal(b2.block()).is_none());
} }
} }

View File

@ -58,6 +58,8 @@ pub struct BasicAuthority {
params: CommonParams, params: CommonParams,
our_params: BasicAuthorityParams, our_params: BasicAuthorityParams,
builtins: BTreeMap<Address, Builtin>, builtins: BTreeMap<Address, Builtin>,
account_provider: Mutex<Option<Arc<AccountProvider>>>,
password: RwLock<Option<String>>,
} }
impl BasicAuthority { impl BasicAuthority {
@ -67,6 +69,8 @@ impl BasicAuthority {
params: params, params: params,
our_params: our_params, our_params: our_params,
builtins: builtins, builtins: builtins,
account_provider: Mutex::new(None),
password: RwLock::new(None),
} }
} }
} }
@ -98,13 +102,8 @@ impl Engine for BasicAuthority {
max(gas_floor_target, gas_limit - gas_limit / bound_divisor + 1.into()) max(gas_floor_target, gas_limit - gas_limit / bound_divisor + 1.into())
} }
}); });
// info!("ethash: populate_from_parent #{}: difficulty={} and gas_limit={}", header.number, header.difficulty, header.gas_limit);
} }
/// Apply the block reward on finalisation of the block.
/// This assumes that all uncles are valid uncles (i.e. of at least one generation before the current).
fn on_close_block(&self, _block: &mut ExecutedBlock) {}
fn is_sealer(&self, author: &Address) -> Option<bool> { fn is_sealer(&self, author: &Address) -> Option<bool> {
Some(self.our_params.authorities.contains(author)) Some(self.our_params.authorities.contains(author))
} }
@ -113,12 +112,12 @@ impl Engine for BasicAuthority {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will /// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned. /// be returned.
fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> {
if let Some(ap) = accounts { if let Some(ref ap) = *self.account_provider.lock() {
let header = block.header(); let header = block.header();
let message = header.bare_hash(); let message = header.bare_hash();
// account should be pernamently unlocked, otherwise sealing will fail // account should be pernamently unlocked, otherwise sealing will fail
if let Ok(signature) = ap.sign(*block.header().author(), None, message) { if let Ok(signature) = ap.sign(*block.header().author(), self.password.read().clone(), message) {
return Some(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]); return Some(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]);
} else { } else {
trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable"); trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable");
@ -179,6 +178,14 @@ impl Engine for BasicAuthority {
fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> { fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
t.sender().map(|_|()) // Perform EC recovery and cache sender t.sender().map(|_|()) // Perform EC recovery and cache sender
} }
fn set_signer(&self, _address: Address, password: String) {
*self.password.write() = Some(password);
}
fn register_account_provider(&self, ap: Arc<AccountProvider>) {
*self.account_provider.lock() = Some(ap);
}
} }
#[cfg(test)] #[cfg(test)]
@ -250,10 +257,11 @@ mod tests {
fn can_generate_seal() { fn can_generate_seal() {
let tap = AccountProvider::transient_provider(); let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("".sha3(), "").unwrap(); let addr = tap.insert_account("".sha3(), "").unwrap();
tap.unlock_account_permanently(addr, "".into()).unwrap();
let spec = new_test_authority(); let spec = new_test_authority();
let engine = &*spec.engine; let engine = &*spec.engine;
engine.set_signer(addr, "".into());
engine.register_account_provider(Arc::new(tap));
let genesis_header = spec.genesis_header(); let genesis_header = spec.genesis_header();
let mut db_result = get_temp_state_db(); let mut db_result = get_temp_state_db();
let mut db = db_result.take(); let mut db = db_result.take();
@ -261,7 +269,7 @@ mod tests {
let last_hashes = Arc::new(vec![genesis_header.hash()]); let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b = b.close_and_lock(); let b = b.close_and_lock();
let seal = engine.generate_seal(b.block(), Some(&tap)).unwrap(); let seal = engine.generate_seal(b.block()).unwrap();
assert!(b.try_seal(engine, seal).is_ok()); assert!(b.try_seal(engine, seal).is_ok());
} }

View File

@ -23,7 +23,6 @@ use spec::CommonParams;
use evm::Schedule; use evm::Schedule;
use block::ExecutedBlock; use block::ExecutedBlock;
use util::Bytes; use util::Bytes;
use account_provider::AccountProvider;
/// An engine which does not provide any consensus mechanism, just seals blocks internally. /// An engine which does not provide any consensus mechanism, just seals blocks internally.
pub struct InstantSeal { pub struct InstantSeal {
@ -60,7 +59,7 @@ impl Engine for InstantSeal {
fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) } fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) }
fn generate_seal(&self, _block: &ExecutedBlock, _accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> {
Some(Vec::new()) Some(Vec::new())
} }
} }
@ -70,16 +69,12 @@ mod tests {
use util::*; use util::*;
use util::trie::TrieSpec; use util::trie::TrieSpec;
use tests::helpers::*; use tests::helpers::*;
use account_provider::AccountProvider;
use spec::Spec; use spec::Spec;
use header::Header; use header::Header;
use block::*; use block::*;
#[test] #[test]
fn instant_can_seal() { fn instant_can_seal() {
let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("".sha3(), "").unwrap();
let spec = Spec::new_instant(); let spec = Spec::new_instant();
let engine = &*spec.engine; let engine = &*spec.engine;
let genesis_header = spec.genesis_header(); let genesis_header = spec.genesis_header();
@ -87,10 +82,9 @@ mod tests {
let mut db = db_result.take(); let mut db = db_result.take();
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap(); spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
let last_hashes = Arc::new(vec![genesis_header.hash()]); let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::default(), (3141562.into(), 31415620.into()), vec![]).unwrap();
let b = b.close_and_lock(); let b = b.close_and_lock();
// Seal with empty AccountProvider. let seal = engine.generate_seal(b.block()).unwrap();
let seal = engine.generate_seal(b.block(), Some(&tap)).unwrap();
assert!(b.try_seal(engine, seal).is_ok()); assert!(b.try_seal(engine, seal).is_ok());
} }

View File

@ -94,7 +94,7 @@ pub trait Engine : Sync + Send {
/// ///
/// This operation is synchronous and may (quite reasonably) not be available, in which None will /// This operation is synchronous and may (quite reasonably) not be available, in which None will
/// be returned. /// be returned.
fn generate_seal(&self, _block: &ExecutedBlock, _accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { None } fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> { None }
/// Phase 1 quick block verification. Only does checks that are cheap. `block` (the header's full block) /// Phase 1 quick block verification. Only does checks that are cheap. `block` (the header's full block)
/// may be provided for additional checks. Returns either a null `Ok` or a general error detailing the problem with import. /// may be provided for additional checks. Returns either a null `Ok` or a general error detailing the problem with import.
@ -147,11 +147,17 @@ pub trait Engine : Sync + Send {
self.builtins().get(a).expect("attempted to execute nonexistent builtin").execute(input, output); self.builtins().get(a).expect("attempted to execute nonexistent builtin").execute(input, output);
} }
/// Add a channel for communication with Client which can be used for sealing.
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
/// Check if new block should be chosen as the one in chain. /// Check if new block should be chosen as the one in chain.
fn is_new_best_block(&self, best_total_difficulty: U256, _best_header: HeaderView, parent_details: &BlockDetails, new_header: &HeaderView) -> bool { fn is_new_best_block(&self, best_total_difficulty: U256, _best_header: HeaderView, parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
ethash::is_new_best_block(best_total_difficulty, parent_details, new_header) ethash::is_new_best_block(best_total_difficulty, parent_details, new_header)
} }
/// Register an account which signs consensus messages.
fn set_signer(&self, _address: Address, _password: String) {}
/// Add a channel for communication with Client which can be used for sealing.
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
/// Add an account provider useful for Engines that sign stuff.
fn register_account_provider(&self, _account_provider: Arc<AccountProvider>) {}
} }

View File

@ -19,11 +19,11 @@ use std::time::{Instant, Duration};
use util::*; use util::*;
use util::using_queue::{UsingQueue, GetAction}; use util::using_queue::{UsingQueue, GetAction};
use account_provider::AccountProvider; use account_provider::{AccountProvider, Error as AccountError};
use views::{BlockView, HeaderView}; use views::{BlockView, HeaderView};
use header::Header; 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, TransactionId};
use client::TransactionImportResult; use client::TransactionImportResult;
use executive::contract_address; use executive::contract_address;
use block::{ClosedBlock, SealedBlock, IsBlock, Block}; use block::{ClosedBlock, SealedBlock, IsBlock, Block};
@ -357,6 +357,8 @@ impl Miner {
let block_number = open_block.block().fields().header.number(); let block_number = open_block.block().fields().header.number();
// TODO Push new uncles too. // TODO Push new uncles too.
let mut tx_count: usize = 0;
let tx_total = transactions.len();
for tx in transactions { for tx in transactions {
let hash = tx.hash(); let hash = tx.hash();
let start = Instant::now(); let start = Instant::now();
@ -378,7 +380,7 @@ impl Miner {
}, },
_ => {}, _ => {},
} }
trace!(target: "miner", "Adding tx {:?} took {:?}", hash, took);
match result { match result {
Err(Error::Execution(ExecutionError::BlockGasLimitReached { gas_limit, gas_used, gas })) => { Err(Error::Execution(ExecutionError::BlockGasLimitReached { gas_limit, gas_used, gas })) => {
debug!(target: "miner", "Skipping adding transaction to block because of gas limit: {:?} (limit: {:?}, used: {:?}, gas: {:?})", hash, gas_limit, gas_used, gas); debug!(target: "miner", "Skipping adding transaction to block because of gas limit: {:?} (limit: {:?}, used: {:?}, gas: {:?})", hash, gas_limit, gas_used, gas);
@ -407,9 +409,12 @@ impl Miner {
"Error adding transaction to block: number={}. transaction_hash={:?}, Error: {:?}", "Error adding transaction to block: number={}. transaction_hash={:?}, Error: {:?}",
block_number, hash, e); block_number, hash, e);
}, },
_ => {} // imported ok _ => {
tx_count += 1;
} // imported ok
} }
} }
trace!(target: "miner", "Pushed {}/{} transactions", tx_count, tx_total);
let block = open_block.close(); let block = open_block.close();
@ -464,15 +469,12 @@ impl Miner {
/// Attempts to perform internal sealing (one that does not require work) to return Ok(sealed), /// Attempts to perform internal sealing (one that does not require work) to return Ok(sealed),
/// Err(Some(block)) returns for unsuccesful sealing while Err(None) indicates misspecified engine. /// Err(Some(block)) returns for unsuccesful sealing while Err(None) indicates misspecified engine.
fn seal_block_internally(&self, block: ClosedBlock) -> Result<SealedBlock, Option<ClosedBlock>> { fn seal_block_internally(&self, block: ClosedBlock) -> Result<SealedBlock, Option<ClosedBlock>> {
trace!(target: "miner", "seal_block_internally: block has transaction - attempting internal seal."); trace!(target: "miner", "seal_block_internally: attempting internal seal.");
let s = self.engine.generate_seal(block.block(), match self.accounts { let s = self.engine.generate_seal(block.block());
Some(ref x) => Some(&**x),
None => None,
});
if let Some(seal) = s { if let Some(seal) = s {
trace!(target: "miner", "seal_block_internally: managed internal seal. importing..."); trace!(target: "miner", "seal_block_internally: managed internal seal. importing...");
block.lock().try_seal(&*self.engine, seal).or_else(|_| { block.lock().try_seal(&*self.engine, seal).or_else(|(e, _)| {
warn!("prepare_sealing: ERROR: try_seal failed when given internally generated seal. WTF?"); warn!("prepare_sealing: ERROR: try_seal failed when given internally generated seal: {}", e);
Err(None) Err(None)
}) })
} else { } else {
@ -485,7 +487,7 @@ impl Miner {
fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool { fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool {
if !block.transactions().is_empty() || self.forced_sealing() { if !block.transactions().is_empty() || self.forced_sealing() {
if let Ok(sealed) = self.seal_block_internally(block) { if let Ok(sealed) = self.seal_block_internally(block) {
if chain.import_block(sealed.rlp_bytes()).is_ok() { if chain.import_sealed_block(sealed).is_ok() {
trace!(target: "miner", "import_block_internally: imported internally sealed block"); trace!(target: "miner", "import_block_internally: imported internally sealed block");
return true return true
} }
@ -583,6 +585,10 @@ impl Miner {
let best_block_header: Header = ::rlp::decode(&chain.best_block_header()); let best_block_header: Header = ::rlp::decode(&chain.best_block_header());
transactions.into_iter() transactions.into_iter()
.map(|tx| { .map(|tx| {
if chain.transaction_block(TransactionID::Hash(tx.hash())).is_some() {
debug!(target: "miner", "Rejected tx {:?}: already in the blockchain", tx.hash());
return Err(Error::Transaction(TransactionError::AlreadyImported));
}
match self.engine.verify_transaction_basic(&tx, &best_block_header) { match self.engine.verify_transaction_basic(&tx, &best_block_header) {
Err(e) => { Err(e) => {
debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e); debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e);
@ -740,6 +746,19 @@ impl MinerService for Miner {
*self.author.write() = author; *self.author.write() = author;
} }
fn set_engine_signer(&self, address: Address, password: String) -> Result<(), AccountError> {
if self.seals_internally {
if let Some(ref ap) = self.accounts {
try!(ap.sign(address.clone(), Some(password.clone()), Default::default()));
}
let mut sealing_work = self.sealing_work.lock();
sealing_work.enabled = self.engine.is_sealer(&address).unwrap_or(false);
*self.author.write() = address;
self.engine.set_signer(address, password);
}
Ok(())
}
fn set_extra_data(&self, extra_data: Bytes) { fn set_extra_data(&self, extra_data: Bytes) {
*self.extra_data.write() = extra_data; *self.extra_data.write() = extra_data;
} }
@ -1042,7 +1061,7 @@ impl MinerService for Miner {
ret.map(f) ret.map(f)
} }
fn submit_seal(&self, chain: &MiningBlockChainClient, pow_hash: H256, seal: Vec<Bytes>) -> Result<(), Error> { fn submit_seal(&self, chain: &MiningBlockChainClient, block_hash: H256, seal: Vec<Bytes>) -> Result<(), Error> {
let result = let result =
if let Some(b) = self.sealing_work.lock().queue.get_used_if( if let Some(b) = self.sealing_work.lock().queue.get_used_if(
if self.options.enable_resubmission { if self.options.enable_resubmission {
@ -1050,22 +1069,22 @@ impl MinerService for Miner {
} else { } else {
GetAction::Take GetAction::Take
}, },
|b| &b.hash() == &pow_hash |b| &b.hash() == &block_hash
) { ) {
trace!(target: "miner", "Sealing block {}={}={} with seal {:?}", pow_hash, b.hash(), b.header().bare_hash(), seal); trace!(target: "miner", "Submitted block {}={}={} with seal {:?}", block_hash, b.hash(), b.header().bare_hash(), seal);
b.lock().try_seal(&*self.engine, seal).or_else(|(e, _)| { b.lock().try_seal(&*self.engine, seal).or_else(|(e, _)| {
warn!(target: "miner", "Mined solution rejected: {}", e); warn!(target: "miner", "Mined solution rejected: {}", e);
Err(Error::PowInvalid) Err(Error::PowInvalid)
}) })
} else { } else {
warn!(target: "miner", "Mined solution rejected: Block unknown or out of date."); warn!(target: "miner", "Submitted solution rejected: Block unknown or out of date.");
Err(Error::PowHashInvalid) Err(Error::PowHashInvalid)
}; };
result.and_then(|sealed| { result.and_then(|sealed| {
let n = sealed.header().number(); let n = sealed.header().number();
let h = sealed.header().hash(); let h = sealed.header().hash();
try!(chain.import_sealed_block(sealed)); try!(chain.import_sealed_block(sealed));
info!(target: "miner", "Mined block imported OK. #{}: {}", Colour::White.bold().paint(format!("{}", n)), Colour::White.bold().paint(h.hex())); info!(target: "miner", "Submitted block imported OK. #{}: {}", Colour::White.bold().paint(format!("{}", n)), Colour::White.bold().paint(h.hex()));
Ok(()) Ok(())
}) })
} }

View File

@ -76,6 +76,9 @@ pub trait MinerService : Send + Sync {
/// Set the author that we will seal blocks as. /// Set the author that we will seal blocks as.
fn set_author(&self, author: Address); fn set_author(&self, author: Address);
/// Set info necessary to sign consensus messages.
fn set_engine_signer(&self, address: Address, password: String) -> Result<(), ::account_provider::Error>;
/// Get the extra_data that we will seal blocks with. /// Get the extra_data that we will seal blocks with.
fn extra_data(&self) -> Bytes; fn extra_data(&self) -> Bytes;

View File

@ -94,7 +94,7 @@ impl Key<blooms::BloomGroup> for TraceGroupPosition {
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]
enum CacheID { enum CacheId {
Trace(H256), Trace(H256),
Bloom(TraceGroupPosition), Bloom(TraceGroupPosition),
} }
@ -104,7 +104,7 @@ pub struct TraceDB<T> where T: DatabaseExtras {
// cache // cache
traces: RwLock<HashMap<H256, FlatBlockTraces>>, traces: RwLock<HashMap<H256, FlatBlockTraces>>,
blooms: RwLock<HashMap<TraceGroupPosition, blooms::BloomGroup>>, blooms: RwLock<HashMap<TraceGroupPosition, blooms::BloomGroup>>,
cache_manager: RwLock<CacheManager<CacheID>>, cache_manager: RwLock<CacheManager<CacheId>>,
// db // db
tracesdb: Arc<Database>, tracesdb: Arc<Database>,
// config, // config,
@ -119,7 +119,7 @@ impl<T> BloomGroupDatabase for TraceDB<T> where T: DatabaseExtras {
fn blooms_at(&self, position: &GroupPosition) -> Option<BloomGroup> { fn blooms_at(&self, position: &GroupPosition) -> Option<BloomGroup> {
let position = TraceGroupPosition::from(position.clone()); let position = TraceGroupPosition::from(position.clone());
let result = self.tracesdb.read_with_cache(db::COL_TRACE, &self.blooms, &position).map(Into::into); let result = self.tracesdb.read_with_cache(db::COL_TRACE, &self.blooms, &position).map(Into::into);
self.note_used(CacheID::Bloom(position)); self.note_used(CacheId::Bloom(position));
result result
} }
} }
@ -152,7 +152,7 @@ impl<T> TraceDB<T> where T: DatabaseExtras {
} }
/// Let the cache system know that a cacheable item has been used. /// Let the cache system know that a cacheable item has been used.
fn note_used(&self, id: CacheID) { fn note_used(&self, id: CacheId) {
let mut cache_manager = self.cache_manager.write(); let mut cache_manager = self.cache_manager.write();
cache_manager.note_used(id); cache_manager.note_used(id);
} }
@ -168,8 +168,8 @@ impl<T> TraceDB<T> where T: DatabaseExtras {
cache_manager.collect_garbage(current_size, | ids | { cache_manager.collect_garbage(current_size, | ids | {
for id in &ids { for id in &ids {
match *id { match *id {
CacheID::Trace(ref h) => { traces.remove(h); }, CacheId::Trace(ref h) => { traces.remove(h); },
CacheID::Bloom(ref h) => { blooms.remove(h); }, CacheId::Bloom(ref h) => { blooms.remove(h); },
} }
} }
traces.shrink_to_fit(); traces.shrink_to_fit();
@ -182,7 +182,7 @@ impl<T> TraceDB<T> where T: DatabaseExtras {
/// Returns traces for block with hash. /// Returns traces for block with hash.
fn traces(&self, block_hash: &H256) -> Option<FlatBlockTraces> { fn traces(&self, block_hash: &H256) -> Option<FlatBlockTraces> {
let result = self.tracesdb.read_with_cache(db::COL_TRACE, &self.traces, block_hash); let result = self.tracesdb.read_with_cache(db::COL_TRACE, &self.traces, block_hash);
self.note_used(CacheID::Trace(block_hash.clone())); self.note_used(CacheId::Trace(block_hash.clone()));
result result
} }
@ -289,7 +289,7 @@ impl<T> TraceDatabase for TraceDB<T> where T: DatabaseExtras {
batch.extend_with_cache(db::COL_TRACE, &mut *blooms, blooms_to_insert, CacheUpdatePolicy::Remove); batch.extend_with_cache(db::COL_TRACE, &mut *blooms, blooms_to_insert, CacheUpdatePolicy::Remove);
// note_used must be called after locking blooms to avoid cache/traces deadlock on garbage collection // note_used must be called after locking blooms to avoid cache/traces deadlock on garbage collection
for key in blooms_keys { for key in blooms_keys {
self.note_used(CacheID::Bloom(key)); self.note_used(CacheId::Bloom(key));
} }
} }
@ -300,7 +300,7 @@ impl<T> TraceDatabase for TraceDB<T> where T: DatabaseExtras {
// cause this value might be queried by hash later // cause this value might be queried by hash later
batch.write_with_cache(db::COL_TRACE, &mut *traces, request.block_hash, request.traces, CacheUpdatePolicy::Overwrite); batch.write_with_cache(db::COL_TRACE, &mut *traces, request.block_hash, request.traces, CacheUpdatePolicy::Overwrite);
// note_used must be called after locking traces to avoid cache/traces deadlock on garbage collection // note_used must be called after locking traces to avoid cache/traces deadlock on garbage collection
self.note_used(CacheID::Trace(request.block_hash.clone())); self.note_used(CacheId::Trace(request.block_hash.clone()));
} }
} }

View File

@ -31,7 +31,7 @@ pub struct KeyFile {
} }
enum KeyFileField { enum KeyFileField {
ID, Id,
Version, Version,
Crypto, Crypto,
Address, Address,
@ -56,7 +56,7 @@ impl Visitor for KeyFileFieldVisitor {
where E: Error where E: Error
{ {
match value { match value {
"id" => Ok(KeyFileField::ID), "id" => Ok(KeyFileField::Id),
"version" => Ok(KeyFileField::Version), "version" => Ok(KeyFileField::Version),
"crypto" => Ok(KeyFileField::Crypto), "crypto" => Ok(KeyFileField::Crypto),
"Crypto" => Ok(KeyFileField::Crypto), "Crypto" => Ok(KeyFileField::Crypto),
@ -94,7 +94,7 @@ impl Visitor for KeyFileVisitor {
loop { loop {
match try!(visitor.visit_key()) { match try!(visitor.visit_key()) {
Some(KeyFileField::ID) => { id = Some(try!(visitor.visit_value())); } Some(KeyFileField::Id) => { id = Some(try!(visitor.visit_value())); }
Some(KeyFileField::Version) => { version = Some(try!(visitor.visit_value())); } Some(KeyFileField::Version) => { version = Some(try!(visitor.visit_value())); }
Some(KeyFileField::Crypto) => { crypto = Some(try!(visitor.visit_value())); } Some(KeyFileField::Crypto) => { crypto = Some(try!(visitor.visit_value())); }
Some(KeyFileField::Address) => { address = Some(try!(visitor.visit_value())); } Some(KeyFileField::Address) => { address = Some(try!(visitor.visit_value())); }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "evmjit" name = "evmjit"
version = "1.4.0" version = "1.5.0"
authors = ["debris <marek.kotewicz@gmail.com>"] authors = ["debris <marek.kotewicz@gmail.com>"]
[lib] [lib]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ethcore-ipc-codegen" name = "ethcore-ipc-codegen"
version = "1.4.0" version = "1.5.0"
authors = ["Nikolay Volf"] authors = ["Nikolay Volf"]
license = "GPL-3.0" license = "GPL-3.0"
description = "Macros to auto-generate implementations for ipc call" description = "Macros to auto-generate implementations for ipc call"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ethcore-ipc-nano" name = "ethcore-ipc-nano"
version = "1.4.0" version = "1.5.0"
authors = ["Nikolay Volf <nikolay@ethcore.io>"] authors = ["Nikolay Volf <nikolay@ethcore.io>"]
license = "GPL-3.0" license = "GPL-3.0"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ethcore-ipc" name = "ethcore-ipc"
version = "1.4.0" version = "1.5.0"
authors = ["Nikolay Volf <nikvolf@gmail.com>"] authors = ["Nikolay Volf <nikvolf@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"

View File

@ -7,6 +7,7 @@
"transform-runtime", "transform-runtime",
"transform-decorators-legacy", "transform-decorators-legacy",
"transform-class-properties", "transform-class-properties",
"transform-object-rest-spread",
"lodash" "lodash"
], ],
"retainLines": true, "retainLines": true,

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.97", "version": "0.2.105",
"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>",
@ -48,25 +48,26 @@
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "6.18.0", "babel-cli": "6.18.0",
"babel-core": "6.18.2", "babel-core": "6.20.0",
"babel-eslint": "7.1.1", "babel-eslint": "7.1.1",
"babel-loader": "6.2.8", "babel-loader": "6.2.8",
"babel-plugin-lodash": "3.2.10", "babel-plugin-lodash": "3.2.10",
"babel-plugin-transform-class-properties": "6.19.0", "babel-plugin-transform-class-properties": "6.18.0",
"babel-plugin-transform-decorators-legacy": "1.3.4", "babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-object-rest-spread": "6.20.2",
"babel-plugin-transform-react-remove-prop-types": "0.2.11", "babel-plugin-transform-react-remove-prop-types": "0.2.11",
"babel-plugin-transform-runtime": "6.15.0", "babel-plugin-transform-runtime": "6.15.0",
"babel-polyfill": "6.16.0", "babel-polyfill": "6.20.0",
"babel-preset-es2015": "6.18.0", "babel-preset-es2015": "6.18.0",
"babel-preset-es2015-rollup": "1.2.0",
"babel-preset-es2016": "6.16.0", "babel-preset-es2016": "6.16.0",
"babel-preset-es2017": "6.16.0", "babel-preset-es2017": "6.16.0",
"babel-preset-react": "6.16.0", "babel-preset-react": "6.16.0",
"babel-preset-stage-0": "6.16.0", "babel-preset-stage-0": "6.16.0",
"babel-register": "6.18.0", "babel-register": "6.18.0",
"babel-runtime": "6.18.0", "babel-runtime": "6.20.0",
"chai": "3.5.0", "chai": "3.5.0",
"chai-enzyme": "0.6.1", "chai-enzyme": "0.6.1",
"circular-dependency-plugin": "2.0.0",
"copy-webpack-plugin": "4.0.1", "copy-webpack-plugin": "4.0.1",
"core-js": "2.4.1", "core-js": "2.4.1",
"coveralls": "2.11.15", "coveralls": "2.11.15",

View File

@ -34,11 +34,18 @@ git fetch origin 2>$GITLOG
git checkout -b $BRANCH git checkout -b $BRANCH
echo "*** Committing compiled files for $UTCDATE" echo "*** Committing compiled files for $UTCDATE"
mv build ../build.new
git add . git add .
git commit -m "$UTCDATE" git commit -m "$UTCDATE [update]"
git merge origin/$BRANCH -X ours --commit -m "$UTCDATE [merge]"
git rm -r build
rm -rf build
git commit -m "$UTCDATE [cleanup]"
mv ../build.new build
git add .
git commit -m "$UTCDATE [release]"
echo "*** Merging remote" echo "*** Merging remote"
git merge origin/$BRANCH -X ours --commit -m "$UTCDATE [release]"
git push origin HEAD:refs/heads/$BRANCH 2>$GITLOG git push origin HEAD:refs/heads/$BRANCH 2>$GITLOG
PRECOMPILED_HASH=`git rev-parse HEAD` PRECOMPILED_HASH=`git rev-parse HEAD`

1
js/scripts/test.js Normal file
View File

@ -0,0 +1 @@
// test script 6

View File

@ -14,10 +14,14 @@
// 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/>.
export const url = (isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io`;
};
export const txLink = (hash, isTestnet = false) => { export const txLink = (hash, isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/tx/${hash}`; return `${url(isTestnet)}/tx/${hash}`;
}; };
export const addressLink = (address, isTestnet = false) => { export const addressLink = (address, isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/address/${address}`; return `${url(isTestnet)}/address/${address}`;
}; };

View File

@ -240,8 +240,8 @@ export default class Contract {
} }
_bindEvent = (event) => { _bindEvent = (event) => {
event.subscribe = (options = {}, callback) => { event.subscribe = (options = {}, callback, autoRemove) => {
return this._subscribe(event, options, callback); return this._subscribe(event, options, callback, autoRemove);
}; };
event.unsubscribe = (subscriptionId) => { event.unsubscribe = (subscriptionId) => {
@ -262,12 +262,11 @@ export default class Contract {
} }
const options = this._getFilterOptions(event, _options); const options = this._getFilterOptions(event, _options);
options.fromBlock = 0;
options.toBlock = 'latest';
return this._api.eth return this._api.eth
.getLogs({ .getLogs(options)
fromBlock: 0,
toBlock: 'latest',
...options
})
.then((logs) => this.parseEventLogs(logs)); .then((logs) => this.parseEventLogs(logs));
} }
@ -307,16 +306,31 @@ export default class Contract {
return this._api.eth.newFilter(options); return this._api.eth.newFilter(options);
} }
subscribe (eventName = null, options = {}, callback) { subscribe (eventName = null, options = {}, callback, autoRemove) {
try { try {
const event = this._findEvent(eventName); const event = this._findEvent(eventName);
return this._subscribe(event, options, callback); return this._subscribe(event, options, callback, autoRemove);
} catch (e) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
} }
} }
_subscribe (event = null, _options, callback) { _sendData (subscriptionId, error, logs) {
const { autoRemove, callback } = this._subscriptions[subscriptionId];
let result = true;
try {
result = callback(error, logs);
} catch (error) {
console.warn('_sendData', subscriptionId, error);
}
if (autoRemove && result && typeof result === 'boolean') {
this.unsubscribe(subscriptionId);
}
}
_subscribe (event = null, _options, callback, autoRemove = false) {
const subscriptionId = nextSubscriptionId++; const subscriptionId = nextSubscriptionId++;
const { skipInitFetch } = _options; const { skipInitFetch } = _options;
delete _options['skipInitFetch']; delete _options['skipInitFetch'];
@ -326,6 +340,7 @@ export default class Contract {
.then((filterId) => { .then((filterId) => {
this._subscriptions[subscriptionId] = { this._subscriptions[subscriptionId] = {
options: _options, options: _options,
autoRemove,
callback, callback,
filterId filterId
}; };
@ -338,8 +353,7 @@ export default class Contract {
return this._api.eth return this._api.eth
.getFilterLogs(filterId) .getFilterLogs(filterId)
.then((logs) => { .then((logs) => {
callback(null, this.parseEventLogs(logs)); this._sendData(subscriptionId, null, this.parseEventLogs(logs));
this._subscribeToChanges(); this._subscribeToChanges();
return subscriptionId; return subscriptionId;
}); });
@ -438,13 +452,13 @@ export default class Contract {
}) })
) )
.then((logsArray) => { .then((logsArray) => {
logsArray.forEach((logs, idx) => { logsArray.forEach((logs, subscriptionId) => {
if (!logs || !logs.length) { if (!logs || !logs.length) {
return; return;
} }
try { try {
subscriptions[idx].callback(null, this.parseEventLogs(logs)); this.sendData(subscriptionId, null, this.parseEventLogs(logs));
} catch (error) { } catch (error) {
console.error('_sendSubscriptionChanges', error); console.error('_sendSubscriptionChanges', error);
} }

View File

@ -112,11 +112,15 @@ export function inNumber10 (number) {
} }
export function inNumber16 (number) { export function inNumber16 (number) {
if (isInstanceOf(number, BigNumber)) { const bn = isInstanceOf(number, BigNumber)
return inHex(number.toString(16)); ? number
: (new BigNumber(number || 0));
if (!bn.isInteger()) {
throw new Error(`[format/input::inNumber16] the given number is not an integer: ${bn.toFormat()}`);
} }
return inHex((new BigNumber(number || 0)).toString(16)); return inHex(bn.toString(16));
} }
export function inOptions (options) { export function inOptions (options) {
@ -130,6 +134,9 @@ export function inOptions (options) {
case 'gas': case 'gas':
case 'gasPrice': case 'gasPrice':
options[key] = inNumber16((new BigNumber(options[key])).round());
break;
case 'value': case 'value':
case 'nonce': case 'nonce':
options[key] = inNumber16(options[key]); options[key] = inNumber16(options[key]);

View File

@ -59,7 +59,7 @@ export default class Manager {
return subscription; return subscription;
} }
subscribe (subscriptionName, callback) { subscribe (subscriptionName, callback, autoRemove = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const subscription = this._validateType(subscriptionName); const subscription = this._validateType(subscriptionName);
@ -75,6 +75,7 @@ export default class Manager {
this.subscriptions[subscriptionId] = { this.subscriptions[subscriptionId] = {
name: subscriptionName, name: subscriptionName,
id: subscriptionId, id: subscriptionId,
autoRemove,
callback callback
}; };
@ -101,13 +102,18 @@ export default class Manager {
} }
_sendData (subscriptionId, error, data) { _sendData (subscriptionId, error, data) {
const { callback } = this.subscriptions[subscriptionId]; const { autoRemove, callback } = this.subscriptions[subscriptionId];
let result = true;
try { try {
callback(error, data); result = callback(error, data);
} catch (error) { } catch (error) {
console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error); console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error);
} }
if (autoRemove && result && typeof result === 'boolean') {
this.unsubscribe(subscriptionId);
}
} }
_updateSubscriptions = (subscriptionName, error, data) => { _updateSubscriptions = (subscriptionName, error, data) => {

View File

@ -68,6 +68,7 @@ export default class Personal {
this._accountsInfo(); this._accountsInfo();
return; return;
case 'parity_removeAddress':
case 'parity_setAccountName': case 'parity_setAccountName':
case 'parity_setAccountMeta': case 'parity_setAccountMeta':
this._accountsInfo(); this._accountsInfo();

View File

@ -14,7 +14,7 @@
// 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 wallet from './wallet'; import { wallet } from './wallet';
export { export {
wallet wallet

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,460 @@
//sol Wallet
// Multi-sig, daily-limited account proxy/wallet.
// @authors:
// Gav Wood <g@ethdev.com>
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a
// single, or, crucially, each of a number of, designated owners.
// usage:
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed.
pragma solidity ^0.4.6;
contract multisig {
// EVENTS
// this contract can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
}
contract multisigAbi is multisig {
function isOwner(address _addr) returns (bool);
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool);
function confirm(bytes32 _h) returns(bool);
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit);
function addOwner(address _owner);
function removeOwner(address _owner);
function changeRequirement(uint _newRequired);
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation);
function changeOwner(address _from, address _to);
function execute(address _to, uint _value, bytes _data) returns(bool);
}
contract WalletLibrary is multisig {
// TYPES
// struct for the status of a pending operation.
struct PendingState {
uint yetNeeded;
uint ownersDone;
uint index;
}
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
/******************************
***** MULTI OWNED SECTION ****
******************************/
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
m_required = _required;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
function isOwner(address _addr) returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
delete m_pendingIndex;
}
/******************************
****** DAY LIMIT SECTION *****
******************************/
// MODIFIERS
// simple modifier for daily limit.
modifier limitedDaily(uint _value) {
if (underLimit(_value))
_;
}
// METHODS
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) {
m_spentToday = 0;
}
// INTERNAL METHODS
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
/******************************
********* WALLET SECTION *****
******************************/
// METHODS
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initMultiowned(_owners, _required);
initDaylimit(_daylimit) ;
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) {
suicide(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) onlyowner returns(bool _callValue) {
// first, take the opportunity to check that we're under the daily limit.
if (underLimit(_value)) {
SingleTransact(msg.sender, _value, _to, _data);
// yes - just execute the call.
_callValue =_to.call.value(_value)(_data);
} else {
// determine our operation hash.
bytes32 _r = sha3(msg.data, block.number);
if (!confirm(_r) && m_txs[_r].to == 0) {
m_txs[_r].to = _to;
m_txs[_r].value = _value;
m_txs[_r].data = _data;
ConfirmationNeeded(_r, msg.sender, _value, _to, _data);
}
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) {
if (m_txs[_h].to != 0) {
m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data);
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function clearWalletPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
delete m_txs[m_pendingIndex[i]];
clearPending();
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint m_required;
// pointer used to find a free slot in m_owners
uint m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
}
contract Wallet is multisig {
// WALLET CONSTRUCTOR
// calls the `initWallet` method of the Library in this context
function Wallet(address[] _owners, uint _required, uint _daylimit) {
// Signature of the Wallet Library's init function
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
address target = _walletLibrary;
// Compute the size of the call data : arrays has 2
// 32bytes for offset and length, plus 32bytes per element ;
// plus 2 32bytes for each uint
uint argarraysize = (2 + _owners.length);
uint argsize = (2 + argarraysize) * 32;
assembly {
// Add the signature first to memory
mstore(0x0, sig)
// Add the call data, which is at the end of the
// code
codecopy(0x4, sub(codesize, argsize), argsize)
// Delegate call to the library
delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
}
}
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
// As return statement unavailable in fallback, explicit the method here
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
function isOwner(address _addr) returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
}

View File

@ -94,7 +94,6 @@ export default class Application extends Component {
tokenregInstance, tokenregInstance,
accounts: Object accounts: Object
.keys(accountsInfo) .keys(accountsInfo)
.filter((address) => !accountsInfo[address].meta.deleted)
.sort((a, b) => { .sort((a, b) => {
return (accountsInfo[b].uuid || '').localeCompare(accountsInfo[a].uuid || ''); return (accountsInfo[b].uuid || '').localeCompare(accountsInfo[a].uuid || '');
}) })

View File

@ -24,7 +24,6 @@ export const fetch = () => (dispatch) => {
.then((accountsInfo) => { .then((accountsInfo) => {
const addresses = Object const addresses = Object
.keys(accountsInfo) .keys(accountsInfo)
.filter((address) => accountsInfo[address] && !accountsInfo[address].meta.deleted)
.map((address) => ({ .map((address) => ({
...accountsInfo[address], ...accountsInfo[address],
address, address,

View File

@ -1,12 +0,0 @@
import babel from 'rollup-plugin-babel';
export default {
entry: 'src/index.js',
dest: 'release/index.js',
format: 'cjs',
plugins: [babel({
babelrc: false,
presets: ['es2015-rollup', 'stage-0'],
runtimeHelpers: true
})]
};

View File

@ -19,7 +19,7 @@ import ContentAdd from 'material-ui/svg-icons/content/add';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, Modal, Form, Input, InputAddress } from '~/ui'; import { Button, Modal, Form, Input, InputAddress } from '~/ui';
import { ERRORS, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAddress, validateName } from '~/util/validation';
export default class AddAddress extends Component { export default class AddAddress extends Component {
static contextTypes = { static contextTypes = {
@ -102,7 +102,7 @@ export default class AddAddress extends Component {
if (!addressError) { if (!addressError) {
const contact = contacts[address]; const contact = contacts[address];
if (contact && !contact.meta.deleted) { if (contact) {
addressError = ERRORS.duplicateAddress; addressError = ERRORS.duplicateAddress;
} }
} }

View File

@ -21,7 +21,7 @@ import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forwa
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
import { eip20, wallet } from '~/contracts/abi'; import { eip20, wallet } from '~/contracts/abi';
@ -231,7 +231,7 @@ export default class AddContract extends Component {
if (!addressError) { if (!addressError) {
const contract = contracts[address]; const contract = contracts[address];
if (contract && !contract.meta.deleted) { if (contract) {
addressError = ERRORS.duplicateAddress; addressError = ERRORS.duplicateAddress;
} }
} }

View File

@ -117,15 +117,17 @@ export default class WalletDetails extends Component {
onChange={ this.onRequiredChange } onChange={ this.onRequiredChange }
param={ parseAbiType('uint') } param={ parseAbiType('uint') }
min={ 1 } min={ 1 }
max={ wallet.owners.length + 1 }
/> />
<TypedInput <TypedInput
label='wallet day limit' label='wallet day limit'
hint='number of days to wait for other owners confirmation' hint='amount of ETH spendable without confirmations'
value={ wallet.daylimit } value={ wallet.daylimit }
error={ errors.daylimit } error={ errors.daylimit }
onChange={ this.onDaylimitChange } onChange={ this.onDaylimitChange }
param={ parseAbiType('uint') } param={ parseAbiType('uint') }
isEth
/> />
</div> </div>
</Form> </Form>

View File

@ -17,6 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui'; import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import { fromWei } from '~/api/util/wei';
import styles from '../createWallet.css'; import styles from '../createWallet.css';
@ -62,7 +63,7 @@ export default class WalletInfo extends Component {
<code>{ required }</code> owners are required to confirm a transaction. <code>{ required }</code> owners are required to confirm a transaction.
</p> </p>
<p> <p>
The daily limit is set to <code>{ daylimit }</code>. The daily limit is set to <code>{ fromWei(daylimit).toFormat() }</code> ETH.
</p> </p>
</CompletedStep> </CompletedStep>
); );

View File

@ -17,6 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { RadioButtons } from '~/ui'; import { RadioButtons } from '~/ui';
import { walletSourceURL } from '~/contracts/code/wallet';
// import styles from '../createWallet.css'; // import styles from '../createWallet.css';
@ -43,7 +44,15 @@ export default class WalletType extends Component {
return [ return [
{ {
label: 'Multi-Sig wallet', key: 'MULTISIG', label: 'Multi-Sig wallet', key: 'MULTISIG',
description: 'A standard multi-signature Wallet' description: (
<span>
<span>Create/Deploy a </span>
<a href={ walletSourceURL } target='_blank'>
standard multi-signature
</a>
<span> Wallet</span>
</span>
)
}, },
{ {
label: 'Watch a wallet', key: 'WATCH', label: 'Watch a wallet', key: 'WATCH',

View File

@ -16,13 +16,13 @@
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import { validateUint, validateAddress, validateName } from '../../util/validation';
import { ERROR_CODES } from '~/api/transport/error';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { ERROR_CODES } from '~/api/transport/error';
import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode } from '~/contracts/code'; import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
import { validateUint, validateAddress, validateName } from '~/util/validation';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
const STEPS = { const STEPS = {
@ -160,14 +160,25 @@ export default class CreateWalletStore {
const { account, owners, required, daylimit } = this.wallet; const { account, owners, required, daylimit } = this.wallet;
const options = { Contracts
data: walletCode, .get()
from: account .registry
}; .lookupAddress(walletLibraryRegKey)
.then((address) => {
const walletLibraryAddress = (address || '').replace(/^0x/, '').toLowerCase();
const code = walletLibraryAddress.length && !/^0+$/.test(walletLibraryAddress)
? walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress)
: fullWalletCode;
this.api const options = {
.newContract(walletAbi) data: code,
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState) from: account
};
return this.api
.newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState);
})
.then((address) => { .then((address) => {
this.deployed = true; this.deployed = true;
this.wallet.address = address; this.wallet.address = address;

View File

@ -18,8 +18,8 @@ import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, Select } 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';
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static contextTypes = { static contextTypes = {

View File

@ -32,7 +32,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Form, TypedInput } from '~/ui'; import { Form, TypedInput } from '~/ui';
import { parseAbiType } from '../../../util/abi'; import { parseAbiType } from '~/util/abi';
import styles from '../deployContract.css'; import styles from '../deployContract.css';

View File

@ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep'; import ParametersStep from './ParametersStep';

View File

@ -19,7 +19,7 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
import ContentSave from 'material-ui/svg-icons/content/save'; import ContentSave from 'material-ui/svg-icons/content/save';
import { Button, Form, Input, InputChip, Modal } from '~/ui'; import { Button, Form, Input, InputChip, Modal } from '~/ui';
import { validateName } from '../../util/validation'; import { validateName } from '~/util/validation';
export default class EditMeta extends Component { export default class EditMeta extends Component {
static contextTypes = { static contextTypes = {

View File

@ -15,12 +15,19 @@
// 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 { Checkbox, MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, InputAddressSelect, Select } from '~/ui'; import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui';
import { parseAbiType } from '~/util/abi';
import styles from '../executeContract.css'; import styles from '../executeContract.css';
const CHECK_STYLE = {
position: 'absolute',
top: '38px',
left: '1em'
};
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
@ -30,10 +37,12 @@ export default class DetailsStep extends Component {
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object, func: PropTypes.object,
funcError: PropTypes.string, funcError: PropTypes.string,
onFuncChange: PropTypes.func, onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired,
warning: PropTypes.string, warning: PropTypes.string,
@ -41,7 +50,7 @@ export default class DetailsStep extends Component {
} }
render () { render () {
const { accounts, amount, amountError, fromAddress, fromAddressError, onFromAddressChange, onAmountChange } = this.props; const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props;
return ( return (
<Form> <Form>
@ -55,12 +64,23 @@ export default class DetailsStep extends Component {
onChange={ onFromAddressChange } /> onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }
<Input <div className={ styles.columns }>
label='transaction value (in ETH)' <div>
hint='the amount to send to with the transaction' <Input
value={ amount } label='transaction value (in ETH)'
error={ amountError } hint='the amount to send to with the transaction'
onSubmit={ onAmountChange } /> value={ amount }
error={ amountError }
onSubmit={ onAmountChange } />
</div>
<div>
<Checkbox
checked={ gasEdit }
label='edit gas price or value'
onCheck={ onGasEditClick }
style={ CHECK_STYLE } />
</div>
</div>
</Form> </Form>
); );
} }
@ -74,7 +94,7 @@ export default class DetailsStep extends Component {
const functions = contract.functions const functions = contract.functions
.filter((func) => !func.constant) .filter((func) => !func.constant)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
.map((func) => { .map((func) => {
const params = (func.abi.inputs || []) const params = (func.abi.inputs || [])
.map((input, index) => { .map((input, index) => {
@ -125,56 +145,22 @@ export default class DetailsStep extends Component {
} }
return (func.abi.inputs || []).map((input, index) => { return (func.abi.inputs || []).map((input, index) => {
const onChange = (event, value) => onValueChange(event, index, value); const onChange = (value) => onValueChange(null, index, value);
const onSelect = (event, _index, value) => onValueChange(event, index, value);
const onSubmit = (value) => onValueChange(null, index, value);
const label = `${input.name}: ${input.type}`; const label = `${input.name}: ${input.type}`;
let inputbox;
switch (input.type) {
case 'address':
inputbox = (
<InputAddressSelect
accounts={ accounts }
editing
label={ label }
value={ values[index] }
error={ valuesError[index] }
onChange={ onChange } />
);
break;
case 'bool':
const boolitems = ['false', 'true'].map((bool) => {
return (
<MenuItem
key={ bool }
value={ bool }
label={ bool }>{ bool }</MenuItem>
);
});
inputbox = (
<Select
label={ label }
value={ values[index] ? 'true' : 'false' }
error={ valuesError[index] }
onChange={ onSelect }>{ boolitems }</Select>
);
break;
default:
inputbox = (
<Input
label={ label }
value={ values[index] }
error={ valuesError[index] }
onSubmit={ onSubmit } />
);
}
return ( return (
<div className={ styles.funcparams } key={ index }> <div
{ inputbox } key={ `${index}_${input.name || ''}` }
className={ styles.funcparams }
>
<TypedInput
label={ label }
value={ values[index] }
error={ valuesError[index] }
onChange={ onChange }
accounts={ accounts }
param={ parseAbiType(input.type) }
/>
</div> </div>
); );
}); });

View File

@ -42,3 +42,15 @@
padding: 0.75em; padding: 0.75em;
text-align: center; text-align: center;
} }
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
&>div {
flex: 0 1 50%;
width: 50%;
position: relative;
}
}

View File

@ -17,18 +17,36 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui';
import { MAX_GAS_ESTIMATION } from '../../util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '../../util/validation'; import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ERRORS from '../Transfer/errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
const STEP_DETAILS = 0;
const STEP_BUSY_OR_GAS = 1;
const STEP_BUSY = 2;
const TITLES = {
transfer: 'function details',
sending: 'sending',
complete: 'complete',
gas: 'gas selection',
rejected: 'rejected'
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_GAS = [TITLES.transfer, TITLES.gas, TITLES.sending, TITLES.complete];
@observer
class ExecuteContract extends Component { class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -45,28 +63,29 @@ class ExecuteContract extends Component {
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} }
gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit);
state = { state = {
amount: '0', amount: '0',
amountError: null, amountError: null,
busyState: null,
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
gas: null, gasEdit: false,
gasLimitError: null, rejected: false,
step: STEP_DETAILS,
sending: false,
values: [], values: [],
valuesError: [], valuesError: [],
step: 0, txhash: null
sending: false,
busyState: null,
txhash: null,
rejected: false
} }
componentDidMount () { componentDidMount () {
const { contract } = this.props; const { contract } = this.props;
const functions = contract.functions const functions = contract.functions
.filter((func) => !func.constant) .filter((func) => !func.constant)
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => (a.name || '').localeCompare(b.name || ''));
this.onFuncChange(null, functions[0]); this.onFuncChange(null, functions[0]);
} }
@ -78,15 +97,21 @@ class ExecuteContract extends Component {
} }
render () { render () {
const { sending } = this.state; const { sending, step, gasEdit, rejected } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='execute function'
busy={ sending } busy={ sending }
waiting={ [1] } current={ step }
visible> steps={ steps }
visible
waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -94,7 +119,7 @@ class ExecuteContract extends Component {
renderDialogActions () { renderDialogActions () {
const { onClose, fromAddress } = this.props; const { onClose, fromAddress } = this.props;
const { sending, step, fromAddressError, valuesError } = this.state; const { gasEdit, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = ( const cancelBtn = (
@ -104,21 +129,44 @@ class ExecuteContract extends Component {
icon={ <ContentClear /> } icon={ <ContentClear /> }
onClick={ onClose } /> onClick={ onClose } />
); );
const postBtn = (
<Button
key='postTransaction'
label='post transaction'
disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
);
const nextBtn = (
<Button
key='nextStep'
label='next'
icon={ <NavigationArrowForward /> }
onClick={ this.onNextClick } />
);
const prevBtn = (
<Button
key='prevStep'
label='prev'
icon={ <NavigationArrowBack /> }
onClick={ this.onPrevClick } />
);
if (step === 0) { if (step === STEP_DETAILS) {
return [ return [
cancelBtn, cancelBtn,
<Button gasEdit ? nextBtn : postBtn
key='postTransaction'
label='post transaction'
disabled={ sending || hasError }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
]; ];
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return [ return [
cancelBtn cancelBtn
]; ];
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return [
cancelBtn,
prevBtn,
postBtn
];
} }
return [ return [
@ -132,7 +180,8 @@ class ExecuteContract extends Component {
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { step, busyState, gasLimitError, txhash, rejected } = this.state; const { gasEdit, step, busyState, txhash, rejected } = this.state;
const { errorEstimated } = this.gasStore;
if (rejected) { if (rejected) {
return ( return (
@ -143,23 +192,29 @@ class ExecuteContract extends Component {
); );
} }
if (step === 0) { if (step === STEP_DETAILS) {
return ( return (
<DetailsStep <DetailsStep
{ ...this.props } { ...this.props }
{ ...this.state } { ...this.state }
warning={ gasLimitError } warning={ errorEstimated }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
onGasEditClick={ this.onGasEditClick }
onValueChange={ this.onValueChange } /> onValueChange={ this.onValueChange } />
); );
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return ( return (
<BusyStep <BusyStep
title='The function execution is in progress' title='The function execution is in progress'
state={ busyState } /> state={ busyState } />
); );
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return (
<GasPriceEditor
store={ this.gasStore } />
);
} }
return ( return (
@ -170,27 +225,14 @@ class ExecuteContract extends Component {
} }
onAmountChange = (amount) => { onAmountChange = (amount) => {
this.gasStore.setEthValue(amount);
this.setState({ amount }, this.estimateGas); this.setState({ amount }, this.estimateGas);
} }
onFuncChange = (event, func) => { onFuncChange = (event, func) => {
const values = func.inputs.map((input) => { const values = (func.abi.inputs || []).map((input) => {
switch (input.kind.type) { const parsedType = parseAbiType(input.type);
case 'address': return parsedType.default;
return '0x';
case 'bool':
return false;
case 'bytes':
return '0x';
case 'uint':
return '0';
default:
return '';
}
}); });
this.setState({ this.setState({
@ -234,7 +276,7 @@ class ExecuteContract extends Component {
estimateGas = (_fromAddress) => { estimateGas = (_fromAddress) => {
const { api } = this.context; const { api } = this.context;
const { fromAddress, gasLimit } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, values } = this.state;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: MAX_GAS_ESTIMATION,
@ -250,18 +292,11 @@ class ExecuteContract extends Component {
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEst) => { .then((gasEst) => {
const gas = gasEst.mul(1.2); const gas = gasEst.mul(1.2);
let gasLimitError = null;
if (gas.gte(MAX_GAS_ESTIMATION)) { console.log(`estimateGas: received ${gasEst.toFormat(0)}, adjusted to ${gas.toFormat(0)}`);
gasLimitError = ERRORS.gasException;
} else if (gas.gt(gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
this.setState({ this.gasStore.setEstimated(gasEst.toFixed(0));
gas, this.gasStore.setGas(gas.toFixed(0));
gasLimitError
});
}) })
.catch((error) => { .catch((error) => {
console.warn('estimateGas', error); console.warn('estimateGas', error);
@ -271,22 +306,20 @@ class ExecuteContract extends Component {
postTransaction = () => { postTransaction = () => {
const { api, store } = this.context; const { api, store } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, gasEdit, values } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
from: fromAddress, from: fromAddress,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}; };
this.setState({ sending: true, step: 1 }); this.setState({ sending: true, step: gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS });
func func
.estimateGas(options, values) .postTransaction(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
console.log(`estimateGas: received ${gas.toFormat(0)}, adjusted to ${gas.mul(1.2).toFormat(0)}`);
return func.postTransaction(options, values);
})
.then((requestId) => { .then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
@ -294,7 +327,7 @@ class ExecuteContract extends Component {
.pollMethod('parity_checkRequest', requestId) .pollMethod('parity_checkRequest', requestId)
.catch((error) => { .catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) { if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true }); this.setState({ rejected: true, step: finalstep });
return false; return false;
} }
@ -302,13 +335,31 @@ class ExecuteContract extends Component {
}); });
}) })
.then((txhash) => { .then((txhash) => {
this.setState({ sending: false, step: 2, txhash, busyState: 'Your transaction has been posted to the network' }); this.setState({ sending: false, step: finalstep, txhash, busyState: 'Your transaction has been posted to the network' });
}) })
.catch((error) => { .catch((error) => {
console.error('postTransaction', error); console.error('postTransaction', error);
store.dispatch({ type: 'newError', error }); store.dispatch({ type: 'newError', error });
}); });
} }
onGasEditClick = () => {
this.setState({
gasEdit: !this.state.gasEdit
});
}
onNextClick = () => {
this.setState({
step: this.state.step + 1
});
}
onPrevClick = () => {
this.setState({
step: this.state.step - 1
});
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {

View File

@ -21,9 +21,8 @@ import { sha3 } from '~/api/util/sha3';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification'; import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification';
import { postToServer } from '../../3rdparty/sms-verification'; import { postToServer } from '~/3rdparty/sms-verification';
import checkIfTxFailed from '../../util/check-if-tx-failed'; import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
import waitForConfirmations from '../../util/wait-for-block-confirmations';
export const LOADING = 'fetching-contract'; export const LOADING = 'fetching-contract';
export const QUERY_DATA = 'query-data'; export const QUERY_DATA = 'query-data';

View File

@ -20,7 +20,7 @@ import SaveIcon from 'material-ui/svg-icons/content/save';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, Modal, Editor, Form, Input } from '~/ui'; import { Button, Modal, Editor, Form, Input } from '~/ui';
import { ERRORS, validateName } from '../../util/validation'; import { ERRORS, validateName } from '~/util/validation';
import styles from './saveContract.css'; import styles from './saveContract.css';

View File

@ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, IdentityIcon, Modal } from '~/ui'; import { Button, IdentityIcon, Modal } from '~/ui';
import initShapeshift from '../../3rdparty/shapeshift'; import initShapeshift from '~/3rdparty/shapeshift';
import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png'; import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png';
import AwaitingDepositStep from './AwaitingDepositStep'; import AwaitingDepositStep from './AwaitingDepositStep';

View File

@ -16,96 +16,28 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Form, { Input } from '~/ui/Form'; import { GasPriceEditor, Form, Input } from '~/ui';
import GasPriceSelector from '../GasPriceSelector';
import styles from '../transfer.css';
export default class Extras extends Component { export default class Extras extends Component {
static propTypes = { static propTypes = {
isEth: PropTypes.bool, isEth: PropTypes.bool,
data: PropTypes.string, data: PropTypes.string,
dataError: PropTypes.string, dataError: PropTypes.string,
gas: PropTypes.string,
gasEst: PropTypes.string,
gasError: PropTypes.string,
gasPrice: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
gasPriceDefault: PropTypes.string,
gasPriceError: PropTypes.string,
gasPriceHistogram: PropTypes.object,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string, totalError: PropTypes.string,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
gasStore: PropTypes.object.isRequired
} }
render () { render () {
const { gas, gasPrice, gasError, gasEst, gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.props; const { gasStore, onChange } = this.props;
const gasLabel = `gas amount (estimated: ${gasEst})`;
const priceLabel = `gas price (current: ${gasPriceDefault})`;
return ( return (
<Form> <Form>
{ this.renderData() } { this.renderData() }
<GasPriceEditor
<div className={ styles.columns }> store={ gasStore }
<div style={ { flex: 65 } }> onChange={ onChange } />
<GasPriceSelector
gasPriceHistogram={ gasPriceHistogram }
gasPrice={ gasPrice }
onChange={ this.onEditGasPrice }
/>
</div>
<div
className={ styles.row }
style={ {
flex: 35, paddingLeft: '1rem',
justifyContent: 'space-around',
paddingBottom: 12
} }
>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ gasError }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ gasPriceError }
value={ (gasPrice || '').toString() }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ totalError }
value={ `${total} ETH` } />
</div>
</div>
</div>
<div>
<p className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transactions' gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</p>
</div>
</Form> </Form>
); );
} }
@ -129,14 +61,6 @@ export default class Extras extends Component {
); );
} }
onEditGas = (event) => {
this.props.onChange('gas', event.target.value);
}
onEditGasPrice = (event, value) => {
this.props.onChange('gasPrice', value);
}
onEditData = (event) => { onEditData = (event) => {
this.props.onChange('data', event.target.value); this.props.onChange('data', event.target.value);
} }

View File

@ -23,7 +23,8 @@ import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store';
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
@ -48,14 +49,6 @@ export default class TransferStore {
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@observable gas = DEFAULT_GAS;
@observable gasError = null;
@observable gasEst = '0';
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = ''; @observable recipient = '';
@observable recipientError = ERRORS.requireRecipient; @observable recipientError = ERRORS.requireRecipient;
@ -68,11 +61,8 @@ export default class TransferStore {
@observable value = '0.0'; @observable value = '0.0';
@observable valueError = null; @observable valueError = null;
gasPriceHistogram = {};
account = null; account = null;
balance = null; balance = null;
gasLimit = null;
onClose = null; onClose = null;
senders = null; senders = null;
@ -81,6 +71,8 @@ export default class TransferStore {
isWallet = false; isWallet = false;
wallet = null; wallet = null;
gasStore = null;
@computed get steps () { @computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -93,7 +85,7 @@ export default class TransferStore {
@computed get isValid () { @computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError; const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.totalError;
const verifyValid = !this.passwordError; const verifyValid = !this.passwordError;
switch (this.stage) { switch (this.stage) {
@ -116,14 +108,14 @@ export default class TransferStore {
this.api = api; this.api = api;
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props; const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose; this.onClose = onClose;
this.isWallet = account && account.wallet; this.isWallet = account && account.wallet;
this.newError = newError; this.newError = newError;
this.gasStore = new GasPriceStore(api, gasLimit);
if (this.isWallet) { if (this.isWallet) {
this.wallet = props.wallet; this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi); this.walletContract = new Contract(this.api, walletAbi);
@ -180,26 +172,6 @@ export default class TransferStore {
} }
} }
@action getDefaults = () => {
Promise
.all([
this.api.parity.gasPriceHistogram(),
this.api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.gasPrice = gasPrice.toString();
this.gasPriceDefault = gasPrice.toFormat();
this.gasPriceHistogram = gasPriceHistogram;
this.recalculate();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
@action onSend = () => { @action onSend = () => {
this.onNext(); this.onNext();
this.sending = true; this.sending = true;
@ -282,25 +254,11 @@ export default class TransferStore {
} }
@action _onUpdateGas = (gas) => { @action _onUpdateGas = (gas) => {
const gasError = this._validatePositiveNumber(gas); this.recalculate();
transaction(() => {
this.gas = gas;
this.gasError = gasError;
this.recalculate();
});
} }
@action _onUpdateGasPrice = (gasPrice) => { @action _onUpdateGasPrice = (gasPrice) => {
const gasPriceError = this._validatePositiveNumber(gasPrice); this.recalculate();
transaction(() => {
this.gasPrice = gasPrice;
this.gasPriceError = gasPriceError;
this.recalculate();
});
} }
@action _onUpdateRecipient = (recipient) => { @action _onUpdateRecipient = (recipient) => {
@ -363,7 +321,7 @@ export default class TransferStore {
@action recalculateGas = () => { @action recalculateGas = () => {
if (!this.isValid) { if (!this.isValid) {
this.gas = 0; this.gasStore.setGas('0');
return this.recalculate(); return this.recalculate();
} }
@ -371,28 +329,20 @@ export default class TransferStore {
.estimateGas() .estimateGas()
.then((gasEst) => { .then((gasEst) => {
let gas = gasEst; let gas = gasEst;
let gasLimitError = null;
if (gas.gt(DEFAULT_GAS)) { if (gas.gt(DEFAULT_GAS)) {
gas = gas.mul(1.2); gas = gas.mul(1.2);
} }
if (gas.gte(MAX_GAS_ESTIMATION)) {
gasLimitError = ERRORS.gasException;
} else if (gas.gt(this.gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
transaction(() => { transaction(() => {
this.gas = gas.toFixed(0); this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasEst = gasEst.toFormat(); this.gasStore.setGas(gas.toFixed(0));
this.gasLimitError = gasLimitError;
this.recalculate(); this.recalculate();
}); });
}) })
.catch((error) => { .catch((error) => {
console.error('etimateGas', error); console.warn('etimateGas', error);
this.recalculate(); this.recalculate();
}); });
} }
@ -412,34 +362,38 @@ export default class TransferStore {
return; return;
} }
const { gas, gasPrice, tag, valueAll, isEth } = this; const { tag, valueAll, isEth, isWallet } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0)); const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0));
const availableEth = new BigNumber(balance.tokens[0].value); const availableEth = new BigNumber(balance.tokens[0].value);
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag); const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const available = new BigNumber(senderBalance.value);
const format = new BigNumber(senderBalance.token.format || 1); const format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
let { value, valueError } = this; let { value, valueError } = this;
let totalEth = gasTotal; let totalEth = gasTotal;
let totalError = null; let totalError = null;
if (valueAll) { if (valueAll) {
if (isEth) { if (isEth && !isWallet) {
const bn = this.api.util.fromWei(availableEth.minus(gasTotal)); const bn = this.api.util.fromWei(availableEth.minus(gasTotal));
value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString(); value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString();
} else if (isEth) {
value = (available.lt(0) ? new BigNumber(0.0) : available).toString();
} else { } else {
value = available.div(format).toString(); value = available.toString();
} }
} }
if (isEth) { if (isEth && !isWallet) {
totalEth = totalEth.plus(this.api.util.toWei(value || 0)); totalEth = totalEth.plus(this.api.util.toWei(value || 0));
} }
if (new BigNumber(value || 0).gt(available.div(format))) { if (new BigNumber(value || 0).gt(available)) {
valueError = ERRORS.largeAmount; valueError = ERRORS.largeAmount;
} else if (valueError === ERRORS.largeAmount) { } else if (valueError === ERRORS.largeAmount) {
valueError = null; valueError = null;
@ -450,10 +404,12 @@ export default class TransferStore {
} }
transaction(() => { transaction(() => {
this.total = this.api.util.fromWei(totalEth).toString(); this.total = this.api.util.fromWei(totalEth).toFixed();
this.totalError = totalError; this.totalError = totalError;
this.value = value; this.value = value;
this.valueError = valueError; this.valueError = valueError;
this.gasStore.setErrorTotal(totalError);
this.gasStore.setEthValue(totalEth);
}); });
} }
@ -519,8 +475,8 @@ export default class TransferStore {
}; };
if (!gas) { if (!gas) {
options.gas = this.gas; options.gas = this.gasStore.gas;
options.gasPrice = this.gasPrice; options.gasPrice = this.gasStore.price;
} else { } else {
options.gas = MAX_GAS_ESTIMATION; options.gas = MAX_GAS_ESTIMATION;
} }

View File

@ -144,15 +144,6 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.chart {
position: absolute;
width: 100%;
}
.gasPriceDesc {
font-size: 0.9em;
}
.warning { .warning {
border-radius: 0.5em; border-radius: 0.5em;
background: #f80; background: #f80;

View File

@ -56,10 +56,6 @@ class Transfer extends Component {
store = new TransferStore(this.context.api, this.props); store = new TransferStore(this.context.api, this.props);
componentDidMount () {
this.store.getDefaults();
}
render () { render () {
const { stage, extras, steps } = this.store; const { stage, extras, steps } = this.store;
@ -139,8 +135,8 @@ class Transfer extends Component {
? ( ? (
<div> <div>
<br /> <br />
<p> <div>
This transaction needs confirmation from other owners. <p>This transaction needs confirmation from other owners.</p>
<Input <Input
style={ { width: '50%', margin: '0 auto' } } style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation } value={ this.store.operation }
@ -148,7 +144,7 @@ class Transfer extends Component {
readOnly readOnly
allowCopy allowCopy
/> />
</p> </div>
</div> </div>
) )
: null : null
@ -186,27 +182,20 @@ class Transfer extends Component {
} }
renderExtrasPage () { renderExtrasPage () {
if (!this.store.gasPriceHistogram) { if (!this.store.gasStore.histogram) {
return null; return null;
} }
const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store; const { isEth, data, dataError, total, totalError } = this.store;
const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store;
return ( return (
<Extras <Extras
isEth={ isEth } isEth={ isEth }
data={ data } data={ data }
dataError={ dataError } dataError={ dataError }
gas={ gas }
gasEst={ gasEst }
gasError={ gasError }
gasPrice={ gasPrice }
gasPriceDefault={ gasPriceDefault }
gasPriceError={ gasPriceError }
gasPriceHistogram={ gasPriceHistogram }
total={ total } total={ total }
totalError={ totalError } totalError={ totalError }
gasStore={ this.store.gasStore }
onChange={ this.store.onUpdateDetails } /> onChange={ this.store.onUpdateDetails } />
); );
} }
@ -263,15 +252,15 @@ class Transfer extends Component {
} }
renderWarning () { renderWarning () {
const { gasLimitError } = this.store; const { errorEstimated } = this.store.gasStore;
if (!gasLimitError) { if (!errorEstimated) {
return null; return null;
} }
return ( return (
<div className={ styles.warning }> <div className={ styles.warning }>
{ gasLimitError } { errorEstimated }
</div> </div>
); );
} }
@ -298,7 +287,6 @@ function mapStateToProps (initState, initProps) {
return (state) => { return (state) => {
const { gasLimit } = state.nodeStatus; const { gasLimit } = state.nodeStatus;
const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null; const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null;
return { gasLimit, wallet, senders, sendersBalances }; return { gasLimit, wallet, senders, sendersBalances };
}; };
} }

View File

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

View File

@ -14,5 +14,50 @@
/* 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/>.
*/ */
.contracts {
.splitInput {
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0 0.25em;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
} }
.change {
background-color: rgba(255, 255, 255, 0.1);
padding: 0.75em 1.75em;
margin-bottom: 1em;
&.add {
background-color: rgba(139, 195, 74, 0.5);
}
&.remove {
background-color: rgba(244, 67, 54, 0.5);
}
.label {
text-transform: uppercase;
margin-bottom: 0.5em;
margin-left: -1em;
font-size: 0.8em;
}
}
.eth:after {
content: 'ETH';
font-size: 0.75em;
margin-left: 0.125em;
}

View File

@ -0,0 +1,321 @@
// 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 { connect } from 'react-redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { parseAbiType } from '~/util/abi';
import { Button, Modal, TxHash, BusyStep, Form, TypedInput, InputAddress, AddressSelect } from '~/ui';
import { fromWei } from '~/api/util/wei';
import WalletSettingsStore from './walletSettingsStore.js';
import styles from './walletSettings.css';
@observer
class WalletSettings extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
senders: PropTypes.object.isRequired
};
store = new WalletSettingsStore(this.context.api, this.props.wallet);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Modal
visible
title='rejected'
actions={ this.renderDialogActions() }
>
<BusyStep
title='The modifications have been rejected'
state='The wallet settings will not be modified. You can safely close this window.'
/>
</Modal>
);
}
return (
<Modal
visible
actions={ this.renderDialogActions() }
current={ stage }
steps={ steps.map((s) => s.title) }
waiting={ waiting }
>
{ this.renderPage() }
</Modal>
);
}
renderPage () {
const { step } = this.store;
switch (step) {
case 'SENDING':
return (
<BusyStep
title='The modifications are currently being sent'
state={ this.store.deployState }
>
{
this.store.requests.map((req) => {
const key = req.id;
if (req.txhash) {
return (<TxHash key={ key } hash={ req.txhash } />);
}
if (req.rejected) {
return (<p key={ key }>The transaction #{parseInt(key, 16)} has been rejected</p>);
}
})
}
</BusyStep>
);
case 'CONFIRMATION':
const { changes } = this.store;
return (
<div>
<p>You are about to make the following modifications</p>
<div>
{ this.renderChanges(changes) }
</div>
</div>
);
default:
case 'EDIT':
const { wallet, errors } = this.store;
const { accounts, senders } = this.props;
return (
<Form>
<p>
In order to edit this contract's settings, at
least { this.store.initialWallet.require.toNumber() } owners have to
send the very same modifications.
Otherwise, no modification will be taken into account...
</p>
<AddressSelect
label='from account (wallet owner)'
hint='send modifications as this owner'
value={ wallet.sender }
error={ errors.sender }
onChange={ this.store.onSenderChange }
accounts={ senders }
/>
<TypedInput
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.store.onOwnersChange }
accounts={ accounts }
param={ parseAbiType('address[]') }
/>
<div className={ styles.splitInput }>
<TypedInput
label='required owners'
hint='number of required owners to accept a transaction'
value={ wallet.require }
error={ errors.require }
onChange={ this.store.onRequireChange }
param={ parseAbiType('uint') }
min={ 1 }
max={ wallet.owners.length }
/>
<TypedInput
label='wallet day limit'
hint='amount of ETH spendable without confirmations'
value={ wallet.dailylimit }
error={ errors.dailylimit }
onChange={ this.store.onDailylimitChange }
param={ parseAbiType('uint') }
isEth
/>
</div>
</Form>
);
}
}
renderChanges (changes) {
return changes.map((change, index) => (
<div key={ `${change.type}_${index}` }>
{ this.renderChange(change) }
</div>
));
}
renderChange (change) {
const { accounts } = this.props;
switch (change.type) {
case 'dailylimit':
return (
<div className={ styles.change }>
<div className={ styles.label }>Change Daily Limit</div>
<div>
<span> from </span>
<code> { fromWei(change.initial).toFormat() }</code>
<span className={ styles.eth } />
<span> to </span>
<code> { fromWei(change.value).toFormat() }</code>
<span className={ styles.eth } />
</div>
</div>
);
case 'require':
return (
<div className={ styles.change }>
<div className={ styles.label }>Change Required Owners</div>
<div>
<span> from </span>
<code> { change.initial.toNumber() }</code>
<span> to </span>
<code> { change.value.toNumber() }</code>
</div>
</div>
);
case 'add_owner':
return (
<div className={ [ styles.change, styles.add ].join(' ') }>
<div className={ styles.label }>Add Owner</div>
<div>
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
/>
</div>
</div>
);
case 'remove_owner':
return (
<div className={ [ styles.change, styles.remove ].join(' ') }>
<div className={ styles.label }>Remove Owner</div>
<div>
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
/>
</div>
</div>
);
}
}
renderDialogActions () {
const { onClose } = this.props;
const { step, hasErrors, rejected, onNext, send, done } = this.store;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ onClose }
/>
);
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ onClose }
/>
);
const sendingBtn = (
<Button
icon={ <ActionDone /> }
label='Sending...'
disabled
/>
);
const nextBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Next'
onClick={ onNext }
disabled={ hasErrors }
/>
);
const sendBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Send'
onClick={ send }
disabled={ hasErrors }
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'SENDING':
return done ? [ closeBtn ] : [ closeBtn, sendingBtn ];
case 'CONFIRMATION':
return [ cancelBtn, sendBtn ];
default:
case 'TYPE':
return [ cancelBtn, nextBtn ];
}
}
}
function mapStateToProps (initState, initProps) {
const { accountsInfo, accounts } = initState.personal;
const { owners } = initProps.wallet;
const senders = pick(accounts, owners);
return () => {
return { accounts: accountsInfo, senders };
};
}
export default connect(mapStateToProps)(WalletSettings);

View File

@ -0,0 +1,306 @@
// 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 { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js';
import { validateUint, validateAddress } from '~/util/validation';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = {
EDIT: { title: 'wallet settings' },
CONFIRMATION: { title: 'confirmation' },
SENDING: { title: 'sending transaction', waiting: true }
};
export default class WalletSettingsStore {
@observable step = null;
@observable requests = [];
@observable deployState = '';
@observable done = false;
@observable wallet = {
owners: null,
require: null,
dailylimit: null,
sender: ''
};
@observable errors = {
owners: null,
require: null,
dailylimit: null,
sender: null
};
@computed get stage () {
return this.stepsKeys.findIndex((k) => k === this.step);
}
@computed get hasErrors () {
return !!Object.keys(this.errors).find((key) => !!this.errors[key]);
}
@computed get stepsKeys () {
return this.steps.map((s) => s.key);
}
@computed get steps () {
return Object
.keys(STEPS)
.map((key) => {
return {
...STEPS[key],
key
};
});
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
get changes () {
const changes = [];
const prevDailylimit = new BigNumber(this.initialWallet.dailylimit);
const nextDailylimit = new BigNumber(this.wallet.dailylimit);
const prevRequire = new BigNumber(this.initialWallet.require);
const nextRequire = new BigNumber(this.wallet.require);
if (!prevDailylimit.equals(nextDailylimit)) {
changes.push({
type: 'dailylimit',
initial: prevDailylimit,
value: nextDailylimit
});
}
if (!prevRequire.equals(nextRequire)) {
changes.push({
type: 'require',
initial: prevRequire,
value: nextRequire
});
}
const prevOwners = this.initialWallet.owners;
const nextOwners = this.wallet.owners;
const ownersToRemove = prevOwners.filter((owner) => !nextOwners.includes(owner));
const ownersToAdd = nextOwners.filter((owner) => !prevOwners.includes(owner));
ownersToRemove.forEach((owner) => {
changes.push({
type: 'remove_owner',
value: owner
});
});
ownersToAdd.forEach((owner) => {
changes.push({
type: 'add_owner',
value: owner
});
});
return changes;
}
constructor (api, wallet) {
this.api = api;
this.step = this.stepsKeys[0];
this.walletInstance = wallet.instance;
this.initialWallet = {
address: wallet.address,
owners: wallet.owners,
require: wallet.require,
dailylimit: wallet.dailylimit.limit
};
transaction(() => {
this.wallet.owners = wallet.owners;
this.wallet.require = wallet.require;
this.wallet.dailylimit = wallet.dailylimit.limit;
this.validateWallet(this.wallet);
});
}
@action onNext = () => {
const stepIndex = this.stepsKeys.findIndex((k) => k === this.step) + 1;
this.step = this.stepsKeys[stepIndex];
}
@action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet);
this.validateWallet(newWallet);
}
@action onOwnersChange = (owners) => {
this.onChange({ owners });
}
@action onRequireChange = (require) => {
this.onChange({ require });
}
@action onSenderChange = (_, sender) => {
this.onChange({ sender });
}
@action onDailylimitChange = (dailylimit) => {
this.onChange({ dailylimit });
}
@action send = () => {
const changes = this.changes;
const walletInstance = this.walletInstance;
this.step = 'SENDING';
this.onTransactionsState('postTransaction');
Promise
.all(changes.map((change) => this.sendChange(change, walletInstance)))
.then((requestIds) => {
this.onTransactionsState('checkRequest');
this.requests = requestIds.map((id) => ({ id, rejected: false, txhash: null }));
return Promise
.all(requestIds.map((id) => {
return this.api
.pollMethod('parity_checkRequest', id)
.then((txhash) => {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].txhash = txhash;
})
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].rejected = true;
return false;
}
throw e;
});
}));
})
.then(() => {
this.done = true;
this.onTransactionsState('completed');
});
}
@action sendChange = (change, walletInstance) => {
const { method, values } = this.getChangeMethod(change, walletInstance);
const options = {
from: this.wallet.sender,
to: this.initialWallet.address,
gas: MAX_GAS_ESTIMATION
};
return method
.estimateGas(options, values)
.then((gasEst) => {
let gas = gasEst;
if (gas.gt(DEFAULT_GAS)) {
gas = gas.mul(1.2);
}
options.gas = gas;
return method.postTransaction(options, values);
});
}
getChangeMethod = (change, walletInstance) => {
if (change.type === 'require') {
return {
method: walletInstance.changeRequirement,
values: [ change.value ]
};
}
if (change.type === 'dailylimit') {
return {
method: walletInstance.setDailyLimit,
values: [ change.value ]
};
}
if (change.type === 'add_owner') {
return {
method: walletInstance.addOwner,
values: [ change.value ]
};
}
if (change.type === 'remove_owner') {
return {
method: walletInstance.removeOwner,
values: [ change.value ]
};
}
}
@action onTransactionsState = (state) => {
switch (state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'completed':
this.deployState = '';
return;
}
}
@action validateWallet = (_wallet) => {
const senderValidation = validateAddress(_wallet.sender);
const requireValidation = validateUint(_wallet.require);
const dailylimitValidation = validateUint(_wallet.dailylimit);
const errors = {
sender: senderValidation.addressError,
require: requireValidation.valueError,
dailylimit: dailylimitValidation.valueError
};
const wallet = {
..._wallet,
sender: senderValidation.address,
require: requireValidation.value,
dailylimit: dailylimitValidation.value
};
transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
}
}

View File

@ -29,6 +29,7 @@ import Transfer from './Transfer';
import PasswordManager from './PasswordManager'; import PasswordManager from './PasswordManager';
import SaveContract from './SaveContract'; import SaveContract from './SaveContract';
import LoadContract from './LoadContract'; import LoadContract from './LoadContract';
import WalletSettings from './WalletSettings';
export { export {
AddAddress, AddAddress,
@ -45,5 +46,6 @@ export {
Transfer, Transfer,
PasswordManager, PasswordManager,
LoadContract, LoadContract,
SaveContract SaveContract,
WalletSettings
}; };

View File

@ -92,7 +92,7 @@ function findImports (path) {
return { error: 'File not found' }; return { error: 'File not found' };
} }
function compile (data) { function compile (data, optimized = 1) {
const { sourcecode, build } = data; const { sourcecode, build } = data;
const { longVersion } = build; const { longVersion } = build;
@ -109,7 +109,7 @@ function compile (data) {
'': sourcecode '': sourcecode
}; };
const compiled = compiler.compile({ sources: input }, 0, findImports); const compiled = compiler.compile({ sources: input }, optimized, findImports);
self.lastCompile = { self.lastCompile = {
version: longVersion, result: compiled, version: longVersion, result: compiled,

View File

@ -23,6 +23,7 @@ export default class Personal {
} }
start () { start () {
this._removeDeleted();
this._subscribeAccountsInfo(); this._subscribeAccountsInfo();
} }
@ -40,4 +41,29 @@ export default class Personal {
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId); console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
}); });
} }
_removeDeleted () {
this._api.parity
.accountsInfo()
.then((accountsInfo) => {
return Promise.all(
Object
.keys(accountsInfo)
.filter((address) => {
const account = accountsInfo[address];
return !account.uuid && account.meta.deleted;
})
.map((address) => this._api.parity.removeAddress(address))
);
})
.then((results) => {
if (results.length) {
console.log(`Removed ${results.length} previously marked addresses`);
}
})
.catch((error) => {
console.warn('removeDeleted', error);
});
}
} }

View File

@ -27,7 +27,7 @@ export function personalAccountsInfo (accountsInfo) {
Object.keys(accountsInfo || {}) Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address })) .map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => !account.meta.deleted) .filter((account) => account.uuid || !account.meta.deleted)
.forEach((account) => { .forEach((account) => {
if (account.uuid) { if (account.uuid) {
accounts[account.address] = account; accounts[account.address] = account;

View File

@ -17,12 +17,10 @@
import { isEqual, uniq } from 'lodash'; import { isEqual, uniq } from 'lodash';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import { wallet as WALLET_ABI } from '~/contracts/abi';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { MAX_GAS_ESTIMATION } from '../../util/constants'; import { wallet as WALLET_ABI } from '~/contracts/abi';
import { MAX_GAS_ESTIMATION } from '~/util/constants';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
@ -58,7 +56,7 @@ function modifyOperation (method, address, owner, operation) {
contract.instance[method] contract.instance[method]
.estimateGas(options, values) .estimateGas(options, values)
.then((gas) => { .then((gas) => {
options.gas = gas; options.gas = gas.mul(1.2);
return contract.instance[method].postTransaction(options, values); return contract.instance[method].postTransaction(options, values);
}) })
.then((requestId) => { .then((requestId) => {
@ -228,7 +226,7 @@ function fetchWalletInfo (contract, update, getState) {
const owners = ownersUpdate && ownersUpdate.value || null; const owners = ownersUpdate && ownersUpdate.value || null;
const transactions = transactionsUpdate && transactionsUpdate.value || null; const transactions = transactionsUpdate && transactionsUpdate.value || null;
return fetchWalletConfirmations(contract, owners, transactions, getState) return fetchWalletConfirmations(contract, update[UPDATE_CONFIRMATIONS], owners, transactions, getState)
.then((update) => { .then((update) => {
updates.push(update); updates.push(update);
return updates; return updates;
@ -292,77 +290,113 @@ function fetchWalletDailylimit (contract) {
}); });
} }
function fetchWalletConfirmations (contract, _owners = null, _transactions = null, getState) { function fetchWalletConfirmations (contract, _operations, _owners = null, _transactions = null, getState) {
const walletInstance = contract.instance; const walletInstance = contract.instance;
const wallet = getState().wallet.wallets[contract.address]; const wallet = getState().wallet.wallets[contract.address];
const owners = _owners || (wallet && wallet.owners) || null; const owners = _owners || (wallet && wallet.owners) || null;
const transactions = _transactions || (wallet && wallet.transactions) || null; const transactions = _transactions || (wallet && wallet.transactions) || null;
// Full load if no operations given, or if the one given aren't loaded yet
const fullLoad = !Array.isArray(_operations) || _operations
.filter((op) => !wallet.confirmations.find((conf) => conf.operation === op))
.length > 0;
return walletInstance let promise;
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
if (comp !== 0) { if (fullLoad) {
return comp; promise = walletInstance
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
if (comp !== 0) {
return comp;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
});
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
} }
return logA.transactionIndex.comparedTo(logB.transactionIndex); // Only fetch confirmations for operations not
// yet confirmed (ie. not yet a transaction)
if (transactions) {
const operations = transactions
.filter((t) => t.operation)
.map((t) => t.operation);
return confirmations.filter((confirmation) => {
return !operations.includes(confirmation.operation);
});
}
return confirmations;
}); });
}) } else {
.then((logs) => { const { confirmations } = wallet;
return logs.map((log) => ({ const nextConfirmations = confirmations
initiator: log.params.initiator.value, .filter((conf) => _operations.includes(conf.operation));
to: log.params.to.value,
data: log.params.data.value, promise = Promise.resolve(nextConfirmations);
value: log.params.value.value, }
operation: bytesToHex(log.params.operation.value),
transactionHash: log.transactionHash, return promise
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((confirmations) => { .then((confirmations) => {
if (confirmations.length === 0) { if (confirmations.length === 0) {
return confirmations; return confirmations;
} }
if (transactions) { const uniqConfirmations = Object.values(
const operations = transactions confirmations.reduce((confirmations, confirmation) => {
.filter((t) => t.operation) confirmations[confirmation.operation] = confirmation;
.map((t) => t.operation); return confirmations;
}, {})
);
return confirmations.filter((confirmation) => { const operations = uniqConfirmations.map((conf) => conf.operation);
return !operations.includes(confirmation.operation);
});
}
return confirmations;
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
const operations = confirmations.map((conf) => conf.operation);
return Promise return Promise
.all(operations.map((op) => fetchOperationConfirmations(contract, op, owners))) .all(operations.map((op) => fetchOperationConfirmations(contract, op, owners)))
.then((confirmedBys) => { .then((confirmedBys) => {
confirmations.forEach((_, index) => { uniqConfirmations.forEach((_, index) => {
confirmations[index].confirmedBy = confirmedBys[index]; uniqConfirmations[index].confirmedBy = confirmedBys[index];
}); });
return confirmations; return uniqConfirmations;
}); });
}) })
.then((confirmations) => { .then((confirmations) => {
const prevConfirmations = wallet.confirmations || [];
const nextConfirmations = prevConfirmations
.filter((conA) => !confirmations.find((conB) => conB.operation === conA.operation))
.concat(confirmations)
.map((conf) => ({
...conf,
pending: false
}));
return { return {
key: UPDATE_CONFIRMATIONS, key: UPDATE_CONFIRMATIONS,
value: confirmations value: nextConfirmations
}; };
}); });
} }
@ -417,7 +451,10 @@ function parseLogs (logs) {
logs.forEach((log) => { logs.forEach((log) => {
const { address, topics } = log; const { address, topics } = log;
const eventSignature = toHex(topics[0]); const eventSignature = toHex(topics[0]);
const prev = updates[address] || { address }; const prev = updates[address] || {
[ UPDATE_DAILYLIMIT ]: true,
address
};
switch (eventSignature) { switch (eventSignature) {
case signatures.OwnerChanged: case signatures.OwnerChanged:
@ -436,16 +473,18 @@ function parseLogs (logs) {
}; };
return; return;
case signatures.ConfirmationNeeded:
case signatures.Confirmation: case signatures.Confirmation:
case signatures.Revoke: case signatures.Revoke:
const operation = log.params.operation.value; const operation = bytesToHex(log.params.operation.value);
updates[address] = { updates[address] = {
...prev, ...prev,
[ UPDATE_CONFIRMATIONS ]: uniq( [ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(operation) (prev[UPDATE_CONFIRMATIONS] || []).concat(operation)
) )
}; };
return; return;
case signatures.Deposit: case signatures.Deposit:
@ -456,17 +495,6 @@ function parseLogs (logs) {
[ UPDATE_TRANSACTIONS ]: true [ UPDATE_TRANSACTIONS ]: true
}; };
return; return;
case signatures.ConfirmationNeeded:
const op = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(op)
)
};
return;
} }
}); });

View File

@ -20,5 +20,14 @@
} }
.data { .data {
flex: 1;
font-family: monospace; font-family: monospace;
padding: 0 0.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.container {
display: flex;
} }

View File

@ -80,7 +80,13 @@ class CopyToClipboard extends Component {
onCopy = () => { onCopy = () => {
const { data, onCopy, cooldown, showSnackbar } = this.props; const { data, onCopy, cooldown, showSnackbar } = this.props;
const message = (<div>copied <code className={ styles.data }>{ data }</code> to clipboard</div>); const message = (
<div className={ styles.container }>
<span>copied </span>
<code className={ styles.data }> { data } </code>
<span> to clipboard</span>
</div>
);
this.setState({ this.setState({
copied: true, copied: true,

View File

@ -170,7 +170,7 @@ export default class AddressSelect extends Component {
handleFilter = (searchText, name, item) => { handleFilter = (searchText, name, item) => {
const { address } = item; const { address } = item;
const entry = this.state.entries[address]; const entry = this.state.entries[address];
const lowCaseSearch = searchText.toLowerCase(); const lowCaseSearch = (searchText || '').toLowerCase();
return [entry.name, entry.address] return [entry.name, entry.address]
.some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1); .some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1);

View File

@ -53,7 +53,6 @@ class InputAddress extends Component {
const { small, allowCopy, hideUnderline, onSubmit, accountsInfo, tokens } = this.props; const { small, allowCopy, hideUnderline, onSubmit, accountsInfo, tokens } = this.props;
const account = accountsInfo[value] || tokens[value]; const account = accountsInfo[value] || tokens[value];
const hasAccount = account && !(account.meta && account.meta.deleted);
const icon = this.renderIcon(); const icon = this.renderIcon();
@ -74,7 +73,7 @@ class InputAddress extends Component {
label={ label } label={ label }
hint={ hint } hint={ hint }
error={ error } error={ error }
value={ text && hasAccount ? account.name : value } value={ text && account ? account.name : value }
onChange={ this.handleInputChange } onChange={ this.handleInputChange }
onSubmit={ onSubmit } onSubmit={ onSubmit }
allowCopy={ allowCopy && (disabled ? value : false) } allowCopy={ allowCopy && (disabled ? value : false) }

View File

@ -29,3 +29,30 @@
position: relative; position: relative;
} }
} }
.ethInput {
display: flex;
flex-direction: row;
align-items: flex-end;
.input {
flex: 1;
position: relative;
.label {
position: absolute;
right: 1.5em;
bottom: 1em;
font-size: 0.85em;
margin-left: 0.5em;
}
}
.toggle {
margin-bottom: 0.5em;
display: flex;
flex-direction: row;
align-items: center;
}
}

View File

@ -15,18 +15,18 @@
// 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 { MenuItem, Toggle } from 'material-ui';
import { range } from 'lodash'; import { range } from 'lodash';
import IconButton from 'material-ui/IconButton'; import IconButton from 'material-ui/IconButton';
import AddIcon from 'material-ui/svg-icons/content/add'; import AddIcon from 'material-ui/svg-icons/content/add';
import RemoveIcon from 'material-ui/svg-icons/content/remove'; import RemoveIcon from 'material-ui/svg-icons/content/remove';
import { fromWei, toWei } from '~/api/util/wei';
import Input from '~/ui/Form/Input'; import Input from '~/ui/Form/Input';
import InputAddressSelect from '~/ui/Form/InputAddressSelect'; import InputAddressSelect from '~/ui/Form/InputAddressSelect';
import Select from '~/ui/Form/Select'; import Select from '~/ui/Form/Select';
import { ABI_TYPES } from '~/util/abi';
import { ABI_TYPES } from '../../../util/abi';
import styles from './typedInput.css'; import styles from './typedInput.css';
@ -42,16 +42,29 @@ export default class TypedInput extends Component {
label: PropTypes.string, label: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
min: PropTypes.number, min: PropTypes.number,
max: PropTypes.number max: PropTypes.number,
isEth: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
min: null, min: null,
max: null max: null,
isEth: false
}; };
state = {
isEth: true,
ethValue: 0
};
componentDidMount () {
if (this.props.isEth && this.props.value) {
this.setState({ ethValue: fromWei(this.props.value) });
}
}
render () { render () {
const { param } = this.props; const { param, isEth } = this.props;
const { type } = param; const { type } = param;
if (type === ABI_TYPES.ARRAY) { if (type === ABI_TYPES.ARRAY) {
@ -87,6 +100,10 @@ export default class TypedInput extends Component {
); );
} }
if (isEth) {
return this.renderEth();
}
return this.renderType(type); return this.renderType(type);
} }
@ -157,16 +174,43 @@ export default class TypedInput extends Component {
return this.renderDefault(); return this.renderDefault();
} }
renderNumber () { renderEth () {
const { label, value, error, param, hint, min, max } = this.props; const { ethValue } = this.state;
const value = ethValue && typeof ethValue.toNumber === 'function'
? ethValue.toNumber()
: ethValue;
return (
<div className={ styles.ethInput }>
<div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div>
<div className={ styles.toggle }>
<Toggle
toggled={ this.state.isEth }
onToggle={ this.onEthTypeChange }
style={ { width: 46 } }
/>
</div>
</div>
);
}
renderNumber (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
return ( return (
<Input <Input
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ value } value={ realValue }
error={ error } error={ error }
onChange={ this.onChange } onChange={ onChange }
type='number' type='number'
min={ min !== null ? min : (param.signed ? null : 0) } min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null } max={ max !== null ? max : null }
@ -236,6 +280,28 @@ export default class TypedInput extends Component {
this.props.onChange(value === 'true'); this.props.onChange(value === 'true');
} }
onEthTypeChange = () => {
const { isEth, ethValue } = this.state;
if (ethValue === '' || ethValue === undefined) {
return this.setState({ isEth: !isEth });
}
const value = isEth ? toWei(ethValue) : fromWei(ethValue);
this.setState({ isEth: !isEth, ethValue: value }, () => {
this.onEthValueChange(null, value);
});
}
onEthValueChange = (event, value) => {
const realValue = this.state.isEth && value !== '' && value !== undefined
? toWei(value)
: value;
this.setState({ ethValue: value });
this.props.onChange(realValue);
}
onChange = (event, value) => { onChange = (event, value) => {
this.props.onChange(value); this.props.onChange(value);
} }

View File

@ -15,3 +15,13 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.chart {
position: absolute;
width: 100%;
}
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
}

View File

@ -29,10 +29,7 @@ import {
import Slider from 'material-ui/Slider'; import Slider from 'material-ui/Slider';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import componentStyles from './gasPriceSelector.css'; import styles from './gasPriceSelector.css';
import mainStyles from '../transfer.css';
const styles = Object.assign({}, mainStyles, componentStyles);
const COLORS = { const COLORS = {
default: 'rgba(255, 99, 132, 0.2)', default: 'rgba(255, 99, 132, 0.2)',
@ -194,10 +191,7 @@ class CustomizedShape extends Component {
class CustomTooltip extends Component { class CustomTooltip extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
type: PropTypes.string, type: PropTypes.string,
payload: PropTypes.array, payload: PropTypes.array,
label: PropTypes.number, label: PropTypes.number,
@ -231,12 +225,16 @@ class CustomTooltip extends Component {
} }
} }
const TOOL_STYLE = {
color: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
padding: '0 0.5em',
fontSize: '0.75em'
};
export default class GasPriceSelector extends Component { export default class GasPriceSelector extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
gasPrice: PropTypes.oneOfType([ gasPrice: PropTypes.oneOfType([
@ -287,21 +285,23 @@ export default class GasPriceSelector extends Component {
renderSlider () { renderSlider () {
const { sliderValue } = this.state; const { sliderValue } = this.state;
return (<div className={ styles.columns }> return (
<Slider <div className={ styles.columns }>
min={ 0 } <Slider
max={ 1 } min={ 0 }
value={ sliderValue } max={ 1 }
onChange={ this.onEditGasPriceSlider } value={ sliderValue }
style={ { onChange={ this.onEditGasPriceSlider }
flex: 1, style={ {
padding: '0 0.3em' flex: 1,
} } padding: '0 0.3em'
sliderStyle={ { } }
marginBottom: 12 sliderStyle={ {
} } marginBottom: 12
/> } }
</div>); />
</div>
);
} }
renderChart () { renderChart () {
@ -316,85 +316,83 @@ export default class GasPriceSelector extends Component {
const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1));
const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]);
return (<div className={ styles.columns }> return (
<div style={ { flex: 1, height } }> <div className={ styles.columns }>
<div className={ styles.chart }> <div style={ { flex: 1, height } }>
<ResponsiveContainer <div className={ styles.chart }>
height={ height } <ResponsiveContainer
> height={ height }
<ScatterChart
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
> >
<Scatter <ScatterChart
data={ [ margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
{ x: sliderValue, y: 0 }, >
{ x: sliderValue, y: selectedCount }, <Scatter
{ x: sliderValue, y: chartData.yDomain[1] } data={ [
] } { x: sliderValue, y: 0 },
shape={ <CustomizedShape showValue={ selectedCount } /> } { x: sliderValue, y: selectedCount },
line { x: sliderValue, y: chartData.yDomain[1] }
isAnimationActive={ false } ] }
/> shape={ <CustomizedShape showValue={ selectedCount } /> }
line
isAnimationActive={ false }
/>
<XAxis <XAxis
hide hide
height={ 0 } height={ 0 }
dataKey='x' dataKey='x'
domain={ [0, 1] } domain={ [0, 1] }
/> />
<YAxis <YAxis
hide hide
width={ 0 } width={ 0 }
dataKey='y' dataKey='y'
domain={ chartData.yDomain } domain={ chartData.yDomain }
/> />
</ScatterChart> </ScatterChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className={ styles.chart }> <div className={ styles.chart }>
<ResponsiveContainer <ResponsiveContainer
height={ height } height={ height }
>
<BarChart
data={ chartData.values }
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
barCategoryGap={ 1 }
ref='barChart'
> >
<Bar <BarChart
dataKey='value' data={ chartData.values }
stroke={ COLORS.line } margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
onClick={ this.onClickGasPrice } barCategoryGap={ 1 }
shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> } ref='barChart'
/> >
<Bar
dataKey='value'
stroke={ COLORS.line }
onClick={ this.onClickGasPrice }
shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> }
/>
<Tooltip <Tooltip
wrapperStyle={ { wrapperStyle={ TOOL_STYLE }
backgroundColor: 'rgba(0, 0, 0, 0.75)', cursor={ this.renderCustomCursor() }
padding: '0 0.5em', content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
fontSize: '0.9em' />
} }
cursor={ this.renderCustomCursor() }
content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
/>
<XAxis <XAxis
hide hide
dataKey='index' dataKey='index'
type='category' type='category'
domain={ chartData.xDomain } domain={ chartData.xDomain }
/> />
<YAxis <YAxis
hide hide
type='number' type='number'
domain={ chartData.yDomain } domain={ chartData.yDomain }
/> />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div>
</div> </div>
</div> </div>
</div>); );
} }
renderCustomCursor = () => { renderCustomCursor = () => {

View File

@ -14,11 +14,36 @@
/* 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/>.
*/ */
.signer {
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
} }
.container { .graphColumn {
flex: 65;
} }
.mainContainer { .editColumn {
flex: 35;
padding-left: 1em;
justify-ontent: space-around;
padding-bottom: 12;
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
}
.gasPriceDesc {
font-size: 0.75em;
opacity: 0.5;
}
.row {
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
} }

View File

@ -0,0 +1,108 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import Input from '../Form/Input';
import GasPriceSelector from './GasPriceSelector';
import Store from './store';
import styles from './gasPriceEditor.css';
@observer
export default class GasPriceEditor extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
store: PropTypes.object.isRequired,
onChange: PropTypes.func
}
static Store = Store;
render () {
const { api } = this.context;
const { store } = this.props;
const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice, errorTotal, totalValue } = store;
const eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`;
return (
<div className={ styles.columns }>
<div className={ styles.graphColumn }>
<GasPriceSelector
gasPriceHistogram={ histogram }
gasPrice={ price }
onChange={ this.onEditGasPrice } />
<div className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transaction gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</div>
</div>
<div className={ styles.editColumn }>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ errorGas }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ errorPrice }
value={ price }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ errorTotal }
value={ `${eth} ETH` } />
</div>
</div>
</div>
);
}
onEditGas = (event, gas) => {
const { store, onChange } = this.props;
store.setGas(gas);
onChange && onChange('gas', gas);
}
onEditGasPrice = (event, price) => {
const { store, onChange } = this.props;
store.setPrice(price);
onChange && onChange('gasPrice', price);
}
}

View File

@ -14,6 +14,4 @@
// You should have received a copy of the GNU General Public License // 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/>.
export default (chain) => { export default from './gasPriceEditor';
return chain === 'morden' || chain === 'ropsten' || chain === 'testnet';
};

View File

@ -0,0 +1,123 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
export default class GasPriceEditor {
@observable errorEstimated = null;
@observable errorGas = null;
@observable errorPrice = null;
@observable errorTotal = null;
@observable estimated = DEFAULT_GAS;
@observable histogram = null;
@observable price = DEFAULT_GASPRICE;
@observable priceDefault = DEFAULT_GASPRICE;
@observable gas = DEFAULT_GAS;
@observable gasLimit = 0;
@observable weiValue = '0';
constructor (api, gasLimit, loadDefaults = true) {
this._api = api;
this.gasLimit = gasLimit;
if (loadDefaults) {
this.loadDefaults();
}
}
@computed get totalValue () {
try {
return new BigNumber(this.gas).mul(this.price).add(this.weiValue);
} catch (error) {
return new BigNumber(0);
}
}
@action setErrorTotal = (errorTotal) => {
this.errorTotal = errorTotal;
}
@action setEstimated = (estimated) => {
transaction(() => {
const bn = new BigNumber(estimated);
this.estimated = estimated;
if (bn.gte(MAX_GAS_ESTIMATION)) {
this.errorEstimated = ERRORS.gasException;
} else if (bn.gte(this.gasLimit)) {
this.errorEstimated = ERRORS.gasBlockLimit;
} else {
this.errorEstimated = null;
}
});
}
@action setEthValue = (weiValue) => {
this.weiValue = weiValue;
}
@action setHistogram = (gasHistogram) => {
this.histogram = gasHistogram;
}
@action setPrice = (price) => {
transaction(() => {
this.errorPrice = validatePositiveNumber(price).numberError;
this.price = price;
});
}
@action setGas = (gas) => {
transaction(() => {
const { numberError } = validatePositiveNumber(gas);
const bn = new BigNumber(gas);
this.gas = gas;
if (numberError) {
this.errorGas = numberError;
} else if (bn.gte(this.gasLimit)) {
this.errorGas = ERRORS.gasBlockLimit;
} else {
this.errorGas = null;
}
});
}
@action loadDefaults () {
Promise
.all([
this._api.parity.gasPriceHistogram(),
this._api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.setPrice(gasPrice.toFixed(0));
this.setHistogram(gasPriceHistogram);
this.priceDefault = gasPrice.toFixed();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
}

View File

@ -37,17 +37,16 @@ class IdentityName extends Component {
render () { render () {
const { address, accountsInfo, tokens, empty, name, shorten, unknown, className } = this.props; const { address, accountsInfo, tokens, empty, name, shorten, unknown, className } = this.props;
const account = accountsInfo[address] || tokens[address]; const account = accountsInfo[address] || tokens[address];
const hasAccount = account && (!account.meta || !account.meta.deleted);
if (!hasAccount && empty) { if (!account && empty) {
return null; return null;
} }
const addressFallback = shorten ? (<ShortenedHash data={ address } />) : address; const addressFallback = shorten ? (<ShortenedHash data={ address } />) : address;
const fallback = unknown ? defaultName : addressFallback; const fallback = unknown ? defaultName : addressFallback;
const isUuid = hasAccount && account.name === account.uuid; const isUuid = account && account.name === account.uuid;
const displayName = (name && name.toUpperCase().trim()) || const displayName = (name && name.toUpperCase().trim()) ||
(hasAccount && !isUuid (account && !isUuid
? account.name.toUpperCase().trim() ? account.name.toUpperCase().trim()
: fallback); : fallback);

View File

@ -20,7 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { LinearProgress } from 'material-ui'; import { LinearProgress } from 'material-ui';
import { txLink } from '../../3rdparty/etherscan/links'; import { txLink } from '~/3rdparty/etherscan/links';
import ShortenedHash from '../ShortenedHash'; import ShortenedHash from '../ShortenedHash';
import styles from './txHash.css'; import styles from './txHash.css';

View File

@ -20,7 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { txLink, addressLink } from '../../3rdparty/etherscan/links'; import { txLink, addressLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import IdentityName from '../IdentityName'; import IdentityName from '../IdentityName';

View File

@ -31,6 +31,7 @@ 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, RadioButtons } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import GasPriceEditor from './GasPriceEditor';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import Loading from './Loading'; import Loading from './Loading';
@ -67,7 +68,7 @@ export {
Errors, Errors,
Form, Form,
FormWrap, FormWrap,
TypedInput, GasPriceEditor,
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,
@ -91,5 +92,6 @@ export {
Tooltip, Tooltip,
Tooltips, Tooltips,
TxHash, TxHash,
TxList TxList,
TypedInput
}; };

View File

@ -1,28 +0,0 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
const checkIfTxFailed = (api, tx, gasSent) => {
return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => {
// TODO: Right now, there's no way to tell wether the EVM code crashed.
// Because you usually send a bit more gas than estimated (to make sure
// it gets mined quickly), we transaction probably failed if all the gas
// has been used up.
return receipt.gasUsed.eq(gasSent);
});
};
export default checkIfTxFailed;

View File

@ -18,7 +18,18 @@ const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
}; };
const waitForConfirmations = (api, tx, confirmations) => { export function checkIfTxFailed (api, tx, gasSent) {
return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => {
// TODO: Right now, there's no way to tell wether the EVM code crashed.
// Because you usually send a bit more gas than estimated (to make sure
// it gets mined quickly), we transaction probably failed if all the gas
// has been used up.
return receipt.gasUsed.eq(gasSent);
});
}
export function waitForConfirmations (api, tx, confirmations) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt) api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt)
.then((receipt) => { .then((receipt) => {
@ -39,6 +50,4 @@ const waitForConfirmations = (api, tx, confirmations) => {
.catch(reject); .catch(reject);
}); });
}); });
}; }
export default waitForConfirmations;

View File

@ -20,6 +20,7 @@ import util from '~/api/util';
export const ERRORS = { export const ERRORS = {
invalidAddress: 'address is an invalid network address', invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
duplicateAddress: 'the address is already in your address book', duplicateAddress: 'the address is already in your address book',
invalidChecksum: 'address has failed the checksum formatting', invalidChecksum: 'address has failed the checksum formatting',
invalidName: 'name should not be blank and longer than 2', invalidName: 'name should not be blank and longer than 2',
@ -27,7 +28,9 @@ export const ERRORS = {
invalidCode: 'code should be the compiled hex string', invalidCode: 'code should be the compiled hex string',
invalidNumber: 'invalid number format', invalidNumber: 'invalid number format',
negativeNumber: 'input number should be positive', negativeNumber: 'input number should be positive',
decimalNumber: 'input number should not contain decimals' decimalNumber: 'input number should not contain decimals',
gasException: 'the transaction will throw an exception with the current values',
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
}; };
export function validateAbi (abi, api) { export function validateAbi (abi, api) {
@ -133,6 +136,25 @@ export function validateName (name) {
}; };
} }
export function validatePositiveNumber (number) {
let numberError = null;
try {
const v = new BigNumber(number);
if (v.lt(0)) {
numberError = ERRORS.invalidAmount;
}
} catch (e) {
numberError = ERRORS.invalidAmount;
}
return {
number,
numberError
};
}
export function validateUint (value) { export function validateUint (value) {
let valueError = null; let valueError = null;
@ -140,7 +162,7 @@ export function validateUint (value) {
const bn = new BigNumber(value); const bn = new BigNumber(value);
if (bn.lt(0)) { if (bn.lt(0)) {
valueError = ERRORS.negativeNumber; valueError = ERRORS.negativeNumber;
} else if (bn.toString().indexOf('.') !== -1) { } else if (!bn.isInteger()) {
valueError = ERRORS.decimalNumber; valueError = ERRORS.decimalNumber;
} }
} catch (e) { } catch (e) {

View File

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

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import etherscan from '../../../3rdparty/etherscan'; import etherscan from '~/3rdparty/etherscan';
import { Container, TxList } from '~/ui'; import { Container, TxList } from '~/ui';
import styles from './transactions.css'; import styles from './transactions.css';

View File

@ -14,8 +14,6 @@
/* 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/>.
*/ */
.account {
}
.btnicon { .btnicon {
width: 24px; width: 24px;

View File

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

View File

@ -24,6 +24,7 @@ import styles from './list.css';
export default class List extends Component { export default class List extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object, accounts: PropTypes.object,
walletsOwners: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
link: PropTypes.string, link: PropTypes.string,
search: PropTypes.array, search: PropTypes.array,
@ -42,7 +43,7 @@ export default class List extends Component {
} }
renderAccounts () { renderAccounts () {
const { accounts, balances, link, empty, handleAddSearchToken } = this.props; const { accounts, balances, link, empty, handleAddSearchToken, walletsOwners } = this.props;
if (empty) { if (empty) {
return ( return (
@ -60,6 +61,8 @@ export default class List extends Component {
const account = accounts[address] || {}; const account = accounts[address] || {};
const balance = balances[address] || {}; const balance = balances[address] || {};
const owners = walletsOwners && walletsOwners[address] || null;
return ( return (
<div <div
className={ styles.item } className={ styles.item }
@ -68,6 +71,7 @@ export default class List extends Component {
link={ link } link={ link }
account={ account } account={ account }
balance={ balance } balance={ balance }
owners={ owners }
handleAddSearchToken={ handleAddSearchToken } /> handleAddSearchToken={ handleAddSearchToken } />
</div> </div>
); );

View File

@ -14,11 +14,16 @@
// 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 React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import ReactTooltip from 'react-tooltip';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes';
import styles from '../accounts.css';
export default class Summary extends Component { export default class Summary extends Component {
static contextTypes = { static contextTypes = {
@ -31,7 +36,8 @@ export default class Summary extends Component {
link: PropTypes.string, link: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
noLink: PropTypes.bool, noLink: PropTypes.bool,
handleAddSearchToken: PropTypes.func handleAddSearchToken: PropTypes.func,
owners: nullableProptype(PropTypes.array)
}; };
static defaultProps = { static defaultProps = {
@ -100,11 +106,42 @@ export default class Summary extends Component {
title={ this.renderLink() } title={ this.renderLink() }
byline={ addressComponent } /> byline={ addressComponent } />
{ this.renderOwners() }
{ this.renderBalance() } { this.renderBalance() }
</Container> </Container>
); );
} }
renderOwners () {
const { owners } = this.props;
const ownersValid = (owners || []).filter((owner) => owner.address && new BigNumber(owner.address).gt(0));
if (!ownersValid || ownersValid.length === 0) {
return null;
}
return (
<div className={ styles.owners }>
{
ownersValid.map((owner) => (
<div key={ owner.address }>
<div
data-tip
data-for={ `owner_${owner.address}` }
data-effect='solid'
>
<IdentityIcon address={ owner.address } button />
</div>
<ReactTooltip id={ `owner_${owner.address}` }>
<strong>{ owner.name } </strong><small> (owner)</small>
</ReactTooltip>
</div>
))
}
</div>
);
}
renderLink () { renderLink () {
const { link, noLink, account, name } = this.props; const { link, noLink, account, name } = this.props;

View File

@ -14,14 +14,18 @@
/* 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/>.
*/ */
.accounts {
}
.accountTooltip { .accountTooltip {
top: 13.3em; top: 13.3em;
left: 7em; left: 7em;
} }
.owners {
margin-top: 1em;
display: flex;
margin-bottom: -0.5em;
}
.toolbar { .toolbar {
position: relative; position: relative;
} }

View File

@ -37,6 +37,7 @@ class Accounts extends Component {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired, hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired, wallets: PropTypes.object.isRequired,
walletsOwners: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired, hasWallets: PropTypes.bool.isRequired,
balances: PropTypes.object balances: PropTypes.object
@ -81,7 +82,7 @@ class Accounts extends Component {
render () { render () {
return ( return (
<div className={ styles.accounts }> <div>
{ this.renderNewDialog() } { this.renderNewDialog() }
{ this.renderNewWalletDialog() } { this.renderNewWalletDialog() }
{ this.renderActionbar() } { this.renderActionbar() }
@ -137,9 +138,13 @@ class Accounts extends Component {
return this.renderLoading(this.props.wallets); return this.renderLoading(this.props.wallets);
} }
const { wallets, hasWallets, balances } = this.props; const { wallets, hasWallets, balances, walletsOwners } = this.props;
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
if (!wallets || Object.keys(wallets).length === 0) {
return null;
}
return ( return (
<List <List
link='wallet' link='wallet'
@ -149,6 +154,7 @@ class Accounts extends Component {
empty={ !hasWallets } empty={ !hasWallets }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
walletsOwners={ walletsOwners }
/> />
); );
} }
@ -281,13 +287,33 @@ class Accounts extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, hasAccounts, wallets, hasWallets } = state.personal; const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal;
const { balances } = state.balances; const { balances } = state.balances;
const walletsInfo = state.wallet.wallets;
const walletsOwners = Object
.keys(walletsInfo)
.map((wallet) => {
const owners = walletsInfo[wallet].owners || [];
return {
owners: owners.map((owner) => ({
address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
})),
address: wallet
};
})
.reduce((walletsOwners, wallet) => {
walletsOwners[wallet.address] = wallet.owners;
return walletsOwners;
}, {});
return { return {
accounts, accounts,
hasAccounts, hasAccounts,
wallets, wallets,
walletsOwners,
hasWallets, hasWallets,
balances balances
}; };

View File

@ -80,10 +80,8 @@ class Delete extends Component {
const { api, router } = this.context; const { api, router } = this.context;
const { account, route, newError } = this.props; const { account, route, newError } = this.props;
account.meta.deleted = true;
api.parity api.parity
.setAccountMeta(account.address, account.meta) .removeAddress(account.address)
.then(() => { .then(() => {
router.push(route); router.push(route);
this.closeDeleteDialog(); this.closeDeleteDialog();

View File

@ -14,37 +14,37 @@
/* 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/>.
*/ */
.address {
}
.delete .hero { .delete {
padding-bottom: 1em; .hero {
} padding-bottom: 1em;
}
.delete .info { .info {
display: inline-block; display: inline-block;
} }
.delete .icon { .icon {
display: inline-block; display: inline-block;
} }
.delete .nameinfo { .nameinfo {
display: inline-block; display: inline-block;
text-align: left; text-align: left;
} }
.delete .header { .header {
text-transform: uppercase; text-transform: uppercase;
font-size: 1.25em; font-size: 1.25em;
padding-bottom: 0.25em; padding-bottom: 0.25em;
} }
.delete .address { .address {
} }
.delete .description { .description {
padding-top: 1em; padding-top: 1em;
font-size: 0.75em; font-size: 0.75em;
color: #aaa; color: #aaa;
}
} }

View File

@ -28,8 +28,6 @@ import Transactions from '../Account/Transactions';
import Delete from './Delete'; import Delete from './Delete';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './address.css';
class Address extends Component { class Address extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -85,7 +83,7 @@ class Address extends Component {
} }
return ( return (
<div className={ styles.address }> <div>
{ this.renderEditDialog(contact) } { this.renderEditDialog(contact) }
{ this.renderActionbar(contact) } { this.renderActionbar(contact) }
<Delete <Delete
@ -121,7 +119,7 @@ class Address extends Component {
return ( return (
<Actionbar <Actionbar
title='Address Information' title='Address Information'
buttons={ !contact || contact.meta.deleted ? [] : buttons } /> buttons={ !contact ? [] : buttons } />
); );
} }

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