Compare commits

...

24 Commits
main ... v1.5.0

Author SHA1 Message Date
Gav
d2e6fc0a65 Dont push-release for beta/stable changes 2017-01-19 14:27:31 +01:00
GitLab Build Bot
29d50f8f30 js-precompiled 20170119-105731 2017-01-19 12:13:13 +01:00
arkpar
536cf73db8 Merge branch 'backporting' into beta 2017-01-19 11:30:32 +01:00
Jaco Greeff
f15a3ca977 Non-secure for DappReg (#4216) 2017-01-19 11:11:19 +01:00
Gav Wood
096a44d9bf Console now has admin (#4220)
Fixes #4210
2017-01-19 10:50:24 +01:00
Jannis Redmann
ff3ae33d99 verification: add mainnet BadgeReg ids (#4190)
* verification: mainnet BadgeReg ids

* verification: fetch contracts by name

* verification: better wording

* typo

* reregistered badges
2017-01-19 10:50:13 +01:00
GitLab Build Bot
8e6d09e6ac [ci skip] js-precompiled 20170119-091230 2017-01-19 09:15:08 +00:00
Tomasz Drwięga
516e16a36b Fixing minimal transaction queue price (#4204)
* Fixing minimal transaction queue price

* Fixing tests
2017-01-18 19:54:40 +01:00
Arkadiy Paronyan
77b14b2736 Fixed --base-path on windows (#4193)
* Fixed --base-path on windows

* Add support for optional args with default text
2017-01-18 19:11:01 +01:00
Tomasz Drwięga
e01636fc34 Fixing etherscan price parsing (#4202)
* Fixing etherscan price parsing

* Handling all errors
2017-01-18 19:10:25 +01:00
arkpar
7b2cfd1cfb JsonRPC bump for IPC fix 2017-01-18 19:10:07 +01:00
Arkadiy Paronyan
cf6d870b09 Backporting to beta (#4203)
* Minor typo to ensure it updates only when synced. (#4188)

* Updater fixes (#4196)

* Minor typo to ensure it updates only when synced.

* Fix deadlock.

* Skip unneeded arg in making list.

* Allow auto-restart even when not running an update.

* Fix trace.

* Update update info on each loop.

* Fix build.

* Shutdown all sockets

* Remove superfluous use.

* Poll for upgrades as part of global status (long) (#4197)

* Poll for upgrades as part of global status (long)

* Fix path

* Prevent duplicate incoming connections (#4180)
2017-01-18 17:56:32 +01:00
Arkadiy Paronyan
f20db41169 gas_limit for blocks, mined by Parity will be divisible by 37 (#4154) (#4176)
* gas_limit for new blocks will divide evenly by 13

* increased PARITY_GAS_LIMIT_DETERMINANT to 37

* separate method for marking mined block

* debug_asserts(gas_limit within protocol range)

* round_block_gas_limit method is now static

* made round_block_gas_limit free-function

* multiplier renamed to multiple
2017-01-16 20:55:07 +01:00
GitLab Build Bot
6830273cb9 [ci skip] js-precompiled 20170116-132441 2017-01-16 13:28:28 +00:00
Arkadiy Paronyan
65594b8865 Backporting to beta (#4175)
* verification: check if server is running (#4140)

* verification: check if server is running

See also ethcore/email-verification#67c6466 and ethcore/sms-verification#a585e42.

* verification: show in the UI if server is running

* verification: code style , more i18n

* fix i18n key

* Optimized hash lookups (#4144)

* Optimize hash comparison

* Use libc

* Ropsten fork detection (#4163)

* Stop flickering + added loader in AddressSelector (#4149)

* Stop UI flickering + added loader to AddressSelector #4103

* PR Grumbles

* Add a password strength component (#4153)

* Added new PasswordStrength Component

* Added tests

* PR Grumbles

* icarus -> update, increase web timeout. (#4165)

* icarus -> update, increase web timeout.

* Fix estimate gas

* Fix token images // Error in Contract Queries (#4169)

* Fix dapps not loading (#4170)

* Add secure to dappsreg

* Remove trailing slash // fix dapps
2017-01-16 13:41:37 +01:00
Tomasz Drwięga
5b30a61158 [beta] Bumping hyper (#4168)
* Bumping hyper

* Bumping again
2017-01-16 10:47:58 +01:00
Denis S. Soldatov aka General-Beck
0bc2aeca8d remove icarus
[ci skip]
2017-01-13 16:28:24 +04:00
GitLab Build Bot
1478109c43 [ci skip] js-precompiled 20170113-100252 2017-01-13 10:05:21 +00:00
Arkadiy Paronyan
bbd2bd0e17 Backporting to beta (#4158)
* Remove onSubmit of current (no auto-change on password edit) (#4151)

* Remove onSubmit from current password

* Remove onSubmit from hint

* Pull in console dapp as builtin (#4145)

* Copy static dapps from static (no build)

* Console sources

* Add console to builtins

* Remove console assets

* Disable eslint on console.js

* Enable eslint after disable

* Webpack copy
2017-01-13 10:55:59 +01:00
Arkadiy Paronyan
1e212771b5 Backporting to beta (#4152)
* Fix broken transfer total balance (#4127)

* Add proper label to method decoding inputs (#4136)

* Another minor estimation fix (#4133)

* Return 0 instead of error with out of gas on estimate_gas

* Fix stuff up.

* Another estimate gas fix.

* Alter balance to maximum possible rather than GP=0.

* Only increase to amount strictly necessary.

* Get rid of unsafe code in ethkey, propagate incorrect Secret errors. (#4119)

* Implementing secret

* Fixing tests

* Refactor VoteCollector (#4101)

* dir

* simple validator list

* stub validator contract

* make the engine hold Weak<Client> instead of IoChannel

* validator set factory

* register weak client with ValidatorContract

* check chain security

* add address array to generator

* register provider contract

* update validator set on notify

* add validator contract spec

* simple list test

* split update and contract test

* contract change

* use client in tendermint

* fix deadlock

* step duration in params

* adapt tendermint tests

* add storage fields to test spec

* constructor spec

* execute under wrong address

* create under correct address

* revert

* validator contract constructor

* move genesis block lookup

* add removal ability to contract

* validator contract adding validators

* fix basic authority

* validator changing test

* more docs

* update sync tests

* remove env_logger

* another env_logger

* cameltoe

* hold EngineClient instead of Client

* return error on misbehaviour

* nicer return

* sprinkle docs

* Reenable mainnet update server. (#4137)

* basic tests for subscribeToEvents (#4115)

* subscribeToEvent fixtures 

* subscribeToEvent tests 

* temporarily skip failing test (#4138)

* Improvements and optimisations to estimate_gas (#4142)

* Return 0 instead of error with out of gas on estimate_gas

* Fix stuff up.

* Another estimate gas fix.

* Alter balance to maximum possible rather than GP=0.

* Only increase to amount strictly necessary.

* Improvements and optimisations to estimate_gas.

- Introduce proper error type
- Avoid building costly traces

* Fix tests.

* Actually fix testsActually fix tests

* Use estimateGas error (as per updated implementation) (#4131)

* Use estimateGas error (as per updated implementation)

* EXCEPTION_ERROR as per #4142

* Better error log reporting & handling (#4128)

* Don't pop-up notifications after network switch (#4076)

* Better notifications

* Don't pollute with notifs if switched networks

* Better connection close/open events / No more notifs on change network

* PR Grumbles

* Add close and open events to HTTP // Add tests

* Fix tests

* WIP Signer Fix

* Fix Signer // Better reconnection handling

* PR Grumbles

* PR Grumbles

* Fixes wrong fetching of balances + Notifications

* Secure API WIP

* Updated Secure API Connection + Status

* Linting

* Linting

* Updated Secure API Logic

* Proper handling of token updates // Fixing poping notifications

* PR Grumbles

* PR Grumbles

* Fixing tests

* Trim spaces from InputAddress (#4126)

* Trim spaces for addresses

* onSubmit has only value, not event

* onSubmit (again)

* Length check on trimmed value

* Remove bindActionCreators({}, dispatch) (empty) (#4135)
2017-01-12 17:06:15 +01:00
GitLab Build Bot
72bb687f5e [ci skip] js-precompiled 20170111-193033 2017-01-11 19:33:04 +00:00
Arkadiy Paronyan
5e70507c78 Backporting to beta (#4118)
* Ignore get_price_info test by default. (#4112)

* Auto-detect hex encoded bytes in sha3 (#4108)

* Auto-detect hex encoded bytes in sha3

* Using types/isHex

* Removing unused imports

* Use binary chop to estimate gas accurately (#4100)

* Initial sketch.

* Building.

* Fix a few things.

* Fix issue, add tracing.

* Address grumbles

* Raise upper limit if needed

* Fix test.

* Fixing decoding API with signatures in names (#4125)

* Fix call/estimate_gas (#4121)

* Return 0 instead of error with out of gas on estimate_gas

* Fix stuff up.
2017-01-11 20:03:08 +01:00
Denis S. Soldatov aka General-Beck
2b588d57f0 Update gitlab-ci.yml
comment curl in arm
remove armv6 build from beta
2017-01-10 19:43:13 +04:00
arkpar
e7fbf10819 Release track set to beta 2017-01-10 15:03:23 +01:00
158 changed files with 3849 additions and 1160 deletions

View File

@@ -38,7 +38,7 @@ linux-stable:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb" --body "parity_"$VER"_amd64.deb"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb.md5" --body "parity_"$VER"_amd64.deb.md5"
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu
tags:
- rust
@@ -107,7 +107,7 @@ linux-centos:
- aws s3 rm --recursive s3://$S3_BUCKET/$CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu
- aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity --body target/release/parity
- aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity.md5 --body parity.md5
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -146,7 +146,7 @@ linux-i686:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_i386.deb" --body "parity_"$VER"_i386.deb"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_i386.deb.md5" --body "parity_"$VER"_i386.deb.md5"
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -192,7 +192,7 @@ linux-armv7:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5"
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -238,7 +238,7 @@ linux-arm:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5"
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://icarus.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -252,10 +252,10 @@ linux-armv6:
stage: build
image: ethcore/rust-armv6:latest
only:
- beta
# - beta
# - tags
# - stable
# - triggers
- triggers
script:
- export CC=arm-linux-gnueabi-gcc
- export CXX=arm-linux-gnueabi-g++
@@ -277,7 +277,7 @@ linux-armv6:
- aws s3 rm --recursive s3://$S3_BUCKET/$CI_BUILD_REF_NAME/$PLATFORM
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity --body target/$PLATFORM/release/parity
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -322,7 +322,7 @@ linux-aarch64:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_arm64.deb" --body "parity_"$VER"_arm64.deb"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity_"$VER"_arm64.deb.md5" --body "parity_"$VER"_arm64.deb.md5"
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- rust
@@ -359,7 +359,7 @@ darwin:
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/parity.md5 --body parity.md5
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity-"$VER"-osx-installer-EXPERIMENTAL.pkg" --body "parity-"$VER"-osx-installer-EXPERIMENTAL.pkg"
- aws s3api put-object --bucket $S3_BUCKET --key $CI_BUILD_REF_NAME/$PLATFORM/"parity-"$VER"-osx-installer-EXPERIMENTAL.pkg.md5" --body "parity-"$VER"-osx-installer-EXPERIMENTAL.pkg.md5"
# - curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1337/push-build/$CI_BUILD_REF_NAME/$PLATFORM
- curl --data "commit=$CI_BUILD_REF&sha3=$SHA3&filename=parity&secret=$RELEASES_SECRET" http://update.parity.io:1338/push-build/$CI_BUILD_REF_NAME/$PLATFORM
tags:
- osx
@@ -421,7 +421,7 @@ windows:
- aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/InstallParity.exe.md5 --body nsis\InstallParity.exe.md5
- aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/win-installer.zip --body nsis\win-installer.zip
- aws s3api put-object --bucket %S3_BUCKET% --key %CI_BUILD_REF_NAME%/x86_64-pc-windows-msvc/win-installer.zip.md5 --body nsis\win-installer.zip.md5
# - curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1337/push-build/%CI_BUILD_REF_NAME%/%PLATFORM%
- curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1337/push-build/%CI_BUILD_REF_NAME%/%PLATFORM%
- curl --data "commit=%CI_BUILD_REF%&sha3=%SHA3%&filename=parity.exe&secret=%RELEASES_SECRET%" http://update.parity.io:1338/push-build/%CI_BUILD_REF_NAME%/%PLATFORM%
tags:
- rust-windows
@@ -529,13 +529,11 @@ js-release:
push-release:
stage: push-release
only:
- beta
- tags
- stable
- triggers
image: ethcore/rust:stable
script:
# - curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1337/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF
- curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1337/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF
- curl --data "secret=$RELEASES_SECRET" http://update.parity.io:1338/push-release/$CI_BUILD_REF_NAME/$CI_BUILD_REF
tags:
- curl

53
Cargo.lock generated
View File

@@ -390,6 +390,7 @@ version = "0.1.2"
dependencies = [
"bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (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)",
]
@@ -413,8 +414,8 @@ dependencies = [
"fetch 0.1.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -582,10 +583,10 @@ dependencies = [
"ethsync 1.5.0",
"fetch 0.1.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-reactor 0.1.0",
"parity-updater 1.5.0",
@@ -610,7 +611,7 @@ dependencies = [
"ethcore-io 1.5.0",
"ethcore-rpc 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?branch=mio-old)",
"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-ui 1.5.0",
@@ -629,8 +630,8 @@ dependencies = [
"ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.5.0",
"ethcore-util 1.5.0",
"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-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"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)",
"mio 0.5.1 (git+https://github.com/ethcore/mio?branch=v0.5.x)",
@@ -879,7 +880,7 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.10.0-a.0"
source = "git+https://github.com/ethcore/hyper#7d4f7fa0baddcb2b0c523f7c05855d67de94fe88"
source = "git+https://github.com/ethcore/hyper#6baea9d444dd1652220ee9b4aeadaebb3de6a955"
dependencies = [
"cookie 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -951,7 +952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonrpc-core"
version = "4.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
source = "git+https://github.com/ethcore/jsonrpc.git?branch=mio-old#057cc58a849d18933a9cce60a33559c7505bd7ce"
dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -963,10 +964,10 @@ dependencies = [
[[package]]
name = "jsonrpc-http-server"
version = "6.1.1"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
source = "git+https://github.com/ethcore/jsonrpc.git?branch=mio-old#057cc58a849d18933a9cce60a33559c7505bd7ce"
dependencies = [
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -974,11 +975,11 @@ dependencies = [
[[package]]
name = "jsonrpc-ipc-server"
version = "0.2.4"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
source = "git+https://github.com/ethcore/jsonrpc.git?branch=mio-old#057cc58a849d18933a9cce60a33559c7505bd7ce"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"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)",
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -989,20 +990,20 @@ dependencies = [
[[package]]
name = "jsonrpc-macros"
version = "0.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
source = "git+https://github.com/ethcore/jsonrpc.git?branch=mio-old#057cc58a849d18933a9cce60a33559c7505bd7ce"
dependencies = [
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "jsonrpc-tcp-server"
version = "0.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#33262d626a294a00c20435dec331058ba65e224a"
source = "git+https://github.com/ethcore/jsonrpc.git?branch=mio-old#057cc58a849d18933a9cce60a33559c7505bd7ce"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"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)",
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1470,7 +1471,7 @@ dependencies = [
"ethcore-signer 1.5.0",
"ethcore-util 1.5.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)",
"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)",
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1501,7 +1502,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#66ef86da6f741953694199813e1eb6b76b3f5b5f"
source = "git+https://github.com/ethcore/js-precompiled.git#35f7fd8782c82081f992fae7f266ce61cb0c95cd"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2414,11 +2415,11 @@ dependencies = [
"checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c"
"checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76"
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
"checksum jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)" = "<none>"
"checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)" = "<none>"
"checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)" = "<none>"
"checksum jsonrpc-macros 0.1.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)" = "<none>"
"checksum jsonrpc-tcp-server 0.1.0 (git+https://github.com/ethcore/jsonrpc.git?branch=mio-old)" = "<none>"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"

View File

@@ -13,8 +13,8 @@ rand = "0.3"
log = "0.3"
env_logger = "0.3"
futures = "0.1"
jsonrpc-core = { git = "https://github.com/ethcore/jsonrpc.git" }
jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc.git" }
jsonrpc-core = { git = "https://github.com/ethcore/jsonrpc.git", branch="mio-old" }
jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc.git", branch="mio-old" }
hyper = { default-features = false, git = "https://github.com/ethcore/hyper" }
unicase = "1.3"
url = "1.0"

View File

@@ -34,7 +34,7 @@ use endpoint::EndpointPath;
use handlers::{ContentHandler, StreamingHandler};
use page::{LocalPageEndpoint, PageHandlerWaiting};
const FETCH_TIMEOUT: u64 = 30;
const FETCH_TIMEOUT: u64 = 300;
pub enum ValidatorResponse {
Local(LocalPageEndpoint),

View File

@@ -24,7 +24,9 @@
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x3"
"networkID" : "0x3",
"forkBlock": 333922,
"forkCanonHash": "0x8737eb141d4f05db57af63fc8d3b4d4d8f9cddb0c4e1ab855de8c288fdc1924f"
},
"genesis": {
"seal": {

View File

@@ -1329,6 +1329,7 @@ mod tests {
use transaction::{Transaction, Action};
use log_entry::{LogEntry, LocalizedLogEntry};
use spec::Spec;
use ethkey::Secret;
fn new_db(path: &str) -> Arc<Database> {
Arc::new(Database::open(&DatabaseConfig::with_columns(::db::NUM_COLUMNS), path).unwrap())
@@ -1467,6 +1468,10 @@ mod tests {
// TODO: insert block that already includes one of them as an uncle to check it's not allowed.
}
fn secret() -> Secret {
Secret::from_slice(&"".sha3()).unwrap()
}
#[test]
fn test_fork_transaction_addresses() {
let mut canon_chain = ChainGenerator::default();
@@ -1482,7 +1487,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let b1a = canon_chain
@@ -1546,7 +1551,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let t2 = Transaction {
nonce: 1.into(),
@@ -1555,7 +1560,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let t3 = Transaction {
nonce: 2.into(),
@@ -1564,7 +1569,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let b1a = canon_chain
.with_transaction(t1.clone())
@@ -1870,7 +1875,7 @@ mod tests {
action: Action::Create,
value: 101.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let t2 = Transaction {
nonce: 0.into(),
gas_price: 0.into(),
@@ -1878,7 +1883,7 @@ mod tests {
action: Action::Create,
value: 102.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let t3 = Transaction {
nonce: 0.into(),
gas_price: 0.into(),
@@ -1886,7 +1891,7 @@ mod tests {
action: Action::Create,
value: 103.into(),
data: "601080600c6000396000f3006000355415600957005b60203560003555".from_hex().unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let tx_hash1 = t1.hash();
let tx_hash2 = t2.hash();
let tx_hash3 = t3.hash();

View File

@@ -873,6 +873,83 @@ impl BlockChainClient for Client {
Ok(ret)
}
fn estimate_gas(&self, t: &SignedTransaction, block: BlockId) -> Result<U256, CallError> {
const UPPER_CEILING: u64 = 1_000_000_000_000u64;
let header = self.block_header(block).ok_or(CallError::StatePruned)?;
let last_hashes = self.build_last_hashes(header.parent_hash());
let env_info = EnvInfo {
number: header.number(),
author: header.author(),
timestamp: header.timestamp(),
difficulty: header.difficulty(),
last_hashes: last_hashes,
gas_used: U256::zero(),
gas_limit: UPPER_CEILING.into(),
};
// that's just a copy of the state.
let original_state = self.state_at(block).ok_or(CallError::StatePruned)?;
let sender = t.sender().map_err(|e| {
let message = format!("Transaction malformed: {:?}", e);
ExecutionError::TransactionMalformed(message)
})?;
let balance = original_state.balance(&sender);
let options = TransactOptions { tracing: true, vm_tracing: false, check_nonce: false };
let mut tx = t.clone();
let mut cond = |gas| {
tx.gas = gas;
let mut state = original_state.clone();
let needed_balance = tx.value + tx.gas * tx.gas_price;
if balance < needed_balance {
// give the sender a sufficient balance
state.add_balance(&sender, &(needed_balance - balance), CleanupMode::NoEmpty);
}
Executive::new(&mut state, &env_info, &*self.engine, &self.factories.vm)
.transact(&tx, options.clone())
.map(|r| r.exception.is_none())
.unwrap_or(false)
};
let mut upper = header.gas_limit();
if !cond(upper) {
// impossible at block gas limit - try `UPPER_CEILING` instead.
// TODO: consider raising limit by powers of two.
upper = UPPER_CEILING.into();
if !cond(upper) {
trace!(target: "estimate_gas", "estimate_gas failed with {}", upper);
return Err(CallError::Execution(ExecutionError::Internal))
}
}
let lower = t.gas_required(&self.engine.schedule(&env_info)).into();
if cond(lower) {
trace!(target: "estimate_gas", "estimate_gas succeeded with {}", lower);
return Ok(lower)
}
/// Find transition point between `lower` and `upper` where `cond` changes from `false` to `true`.
/// Returns the lowest value between `lower` and `upper` for which `cond` returns true.
/// We assert: `cond(lower) = false`, `cond(upper) = true`
fn binary_chop<F>(mut lower: U256, mut upper: U256, mut cond: F) -> U256 where F: FnMut(U256) -> bool {
while upper - lower > 1.into() {
let mid = (lower + upper) / 2.into();
trace!(target: "estimate_gas", "{} .. {} .. {}", lower, mid, upper);
let c = cond(mid);
match c {
true => upper = mid,
false => lower = mid,
};
trace!(target: "estimate_gas", "{} => {} .. {}", c, lower, upper);
}
upper
}
// binary chop to non-excepting call with gas somewhere between 21000 and block gas limit
trace!(target: "estimate_gas", "estimate_gas chopping {} .. {}", lower, upper);
Ok(binary_chop(lower, upper, cond))
}
fn replay(&self, id: TransactionId, analytics: CallAnalytics) -> Result<Executed, CallError> {
let address = self.transaction_address(id).ok_or(CallError::TransactionNotFound)?;
let header = self.block_header(BlockId::Hash(address.block_hash)).ok_or(CallError::StatePruned)?;
@@ -1594,7 +1671,7 @@ mod tests {
use util::Hashable;
// given
let key = KeyPair::from_secret("test".sha3()).unwrap();
let key = KeyPair::from_secret_slice(&"test".sha3()).unwrap();
let secret = key.secret();
let block_number = 1;

View File

@@ -242,7 +242,7 @@ impl TestBlockChainClient {
value: U256::from(100),
data: "3331600055".from_hex().unwrap(),
gas: U256::from(100_000),
gas_price: U256::one(),
gas_price: U256::from(200_000_000_000u64),
nonce: U256::zero()
};
let signed_tx = tx.sign(keypair.secret(), None);
@@ -308,11 +308,11 @@ impl TestBlockChainClient {
value: U256::from(100),
data: "3331600055".from_hex().unwrap(),
gas: U256::from(100_000),
gas_price: U256::one(),
gas_price: U256::from(20_000_000_000u64),
nonce: U256::zero()
};
let signed_tx = tx.sign(keypair.secret(), None);
self.set_balance(signed_tx.sender().unwrap(), 10_000_000.into());
self.set_balance(signed_tx.sender().unwrap(), 10_000_000_000_000_000_000u64.into());
let res = self.miner.import_external_transactions(self, vec![signed_tx]);
let res = res.into_iter().next().unwrap().expect("Successful import");
assert_eq!(res, TransactionImportResult::Current);
@@ -379,6 +379,10 @@ impl BlockChainClient for TestBlockChainClient {
self.execution_result.read().clone().unwrap()
}
fn estimate_gas(&self, _t: &SignedTransaction, _block: BlockId) -> Result<U256, CallError> {
Ok(21000.into())
}
fn replay(&self, _id: TransactionId, _analytics: CallAnalytics) -> Result<Executed, CallError> {
self.execution_result.read().clone().unwrap()
}

View File

@@ -184,6 +184,9 @@ pub trait BlockChainClient : Sync + Send {
/// Makes a non-persistent transaction call.
fn call(&self, t: &SignedTransaction, block: BlockId, analytics: CallAnalytics) -> Result<Executed, CallError>;
/// Estimates how much gas will be necessary for a call.
fn estimate_gas(&self, t: &SignedTransaction, block: BlockId) -> Result<U256, CallError>;
/// Replays a given transaction for inspection.
fn replay(&self, t: TransactionId, analytics: CallAnalytics) -> Result<Executed, CallError>;

View File

@@ -354,6 +354,7 @@ mod tests {
use env_info::EnvInfo;
use header::Header;
use error::{Error, BlockError};
use ethkey::Secret;
use rlp::encode;
use block::*;
use tests::helpers::*;
@@ -411,8 +412,8 @@ mod tests {
#[test]
fn generates_seal_and_does_not_double_propose() {
let tap = AccountProvider::transient_provider();
let addr1 = tap.insert_account("1".sha3(), "1").unwrap();
let addr2 = tap.insert_account("2".sha3(), "2").unwrap();
let addr1 = tap.insert_account(Secret::from_slice(&"1".sha3()).unwrap(), "1").unwrap();
let addr2 = tap.insert_account(Secret::from_slice(&"2".sha3()).unwrap(), "2").unwrap();
let spec = Spec::new_test_round();
let engine = &*spec.engine;
@@ -445,7 +446,7 @@ mod tests {
fn proposer_switching() {
let mut header: Header = Header::default();
let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("0".sha3(), "0").unwrap();
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
header.set_author(addr);
@@ -464,7 +465,7 @@ mod tests {
fn rejects_future_block() {
let mut header: Header = Header::default();
let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("0".sha3(), "0").unwrap();
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
header.set_author(addr);

View File

@@ -201,6 +201,7 @@ mod tests {
use error::{BlockError, Error};
use tests::helpers::*;
use account_provider::AccountProvider;
use ethkey::Secret;
use header::Header;
use spec::Spec;
use engines::Seal;
@@ -261,7 +262,7 @@ mod tests {
#[test]
fn can_generate_seal() {
let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("".sha3(), "").unwrap();
let addr = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap();
let spec = new_test_authority();
let engine = &*spec.engine;
@@ -281,7 +282,7 @@ mod tests {
#[test]
fn seals_internally() {
let tap = AccountProvider::transient_provider();
let authority = tap.insert_account("".sha3(), "").unwrap();
let authority = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap();
let engine = new_test_authority().engine;
assert!(!engine.is_sealer(&Address::default()).unwrap());

View File

@@ -23,15 +23,35 @@ use header::Header;
use rlp::*;
use ethkey::{recover, public_to_address};
#[derive(Debug, PartialEq, Eq, Clone)]
/// Message transmitted between consensus participants.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct ConsensusMessage {
pub vote_step: VoteStep,
pub block_hash: Option<BlockHash>,
pub signature: H520,
}
/// Complete step of the consensus process.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct VoteStep {
pub height: Height,
pub round: Round,
pub step: Step,
pub block_hash: Option<BlockHash>,
}
impl VoteStep {
pub fn new(height: Height, round: Round, step: Step) -> Self {
VoteStep { height: height, round: round, step: step }
}
pub fn is_height(&self, height: Height) -> bool {
self.height == height
}
pub fn is_round(&self, height: Height, round: Round) -> bool {
self.height == height && self.round == round
}
}
fn consensus_round(header: &Header) -> Result<Round, ::rlp::DecoderError> {
let round_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed");
@@ -42,53 +62,29 @@ impl ConsensusMessage {
pub fn new(signature: H520, height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Self {
ConsensusMessage {
signature: signature,
height: height,
round: round,
step: step,
block_hash: block_hash,
vote_step: VoteStep::new(height, round, step),
}
}
pub fn new_proposal(header: &Header) -> Result<Self, ::rlp::DecoderError> {
Ok(ConsensusMessage {
vote_step: VoteStep::new(header.number() as Height, consensus_round(header)?, Step::Propose),
signature: UntrustedRlp::new(header.seal().get(1).expect("seal passed basic verification; seal has 3 fields; qed").as_slice()).as_val()?,
height: header.number() as Height,
round: consensus_round(header)?,
step: Step::Propose,
block_hash: Some(header.bare_hash()),
})
}
pub fn new_commit(proposal: &ConsensusMessage, signature: H520) -> Self {
let mut vote_step = proposal.vote_step.clone();
vote_step.step = Step::Precommit;
ConsensusMessage {
signature: signature,
height: proposal.height,
round: proposal.round,
step: Step::Precommit,
vote_step: vote_step,
block_hash: proposal.block_hash,
signature: signature,
}
}
pub fn is_height(&self, height: Height) -> bool {
self.height == height
}
pub fn is_round(&self, height: Height, round: Round) -> bool {
self.height == height && self.round == round
}
pub fn is_step(&self, height: Height, round: Round, step: Step) -> bool {
self.height == height && self.round == round && self.step == step
}
pub fn is_block_hash(&self, h: Height, r: Round, s: Step, block_hash: Option<BlockHash>) -> bool {
self.height == h && self.round == r && self.step == s && self.block_hash == block_hash
}
pub fn is_aligned(&self, m: &ConsensusMessage) -> bool {
self.is_block_hash(m.height, m.round, m.step, m.block_hash)
}
pub fn verify(&self) -> Result<Address, Error> {
let full_rlp = ::rlp::encode(self);
let block_info = Rlp::new(&full_rlp).at(1);
@@ -97,16 +93,30 @@ impl ConsensusMessage {
}
pub fn precommit_hash(&self) -> H256 {
message_info_rlp(self.height, self.round, Step::Precommit, self.block_hash).sha3()
let mut vote_step = self.vote_step.clone();
vote_step.step = Step::Precommit;
message_info_rlp(&vote_step, self.block_hash).sha3()
}
}
impl PartialOrd for ConsensusMessage {
fn partial_cmp(&self, m: &ConsensusMessage) -> Option<Ordering> {
impl PartialOrd for VoteStep {
fn partial_cmp(&self, m: &VoteStep) -> Option<Ordering> {
Some(self.cmp(m))
}
}
impl Ord for VoteStep {
fn cmp(&self, m: &VoteStep) -> Ordering {
if self.height != m.height {
self.height.cmp(&m.height)
} else if self.round != m.round {
self.round.cmp(&m.round)
} else {
self.step.number().cmp(&m.step.number())
}
}
}
impl Step {
fn number(&self) -> u8 {
match *self {
@@ -118,20 +128,6 @@ impl Step {
}
}
impl Ord for ConsensusMessage {
fn cmp(&self, m: &ConsensusMessage) -> Ordering {
if self.height != m.height {
self.height.cmp(&m.height)
} else if self.round != m.round {
self.round.cmp(&m.round)
} else if self.step != m.step {
self.step.number().cmp(&m.step.number())
} else {
self.signature.cmp(&m.signature)
}
}
}
impl Decodable for Step {
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
match decoder.as_rlp().as_val()? {
@@ -149,42 +145,39 @@ impl Encodable for Step {
}
}
/// (signature, height, round, step, block_hash)
/// (signature, (height, round, step, block_hash))
impl Decodable for ConsensusMessage {
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
let rlp = decoder.as_rlp();
let m = rlp.at(1)?;
let block_message: H256 = m.val_at(3)?;
Ok(ConsensusMessage {
signature: rlp.val_at(0)?,
height: m.val_at(0)?,
round: m.val_at(1)?,
step: m.val_at(2)?,
vote_step: VoteStep::new(m.val_at(0)?, m.val_at(1)?, m.val_at(2)?),
block_hash: match block_message.is_zero() {
true => None,
false => Some(block_message),
}
},
signature: rlp.val_at(0)?,
})
}
}
}
impl Encodable for ConsensusMessage {
fn rlp_append(&self, s: &mut RlpStream) {
let info = message_info_rlp(self.height, self.round, self.step, self.block_hash);
let info = message_info_rlp(&self.vote_step, self.block_hash);
s.begin_list(2)
.append(&self.signature)
.append_raw(&info, 1);
}
}
pub fn message_info_rlp(height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Bytes {
pub fn message_info_rlp(vote_step: &VoteStep, block_hash: Option<BlockHash>) -> Bytes {
// TODO: figure out whats wrong with nested list encoding
let mut s = RlpStream::new_list(5);
s.append(&height).append(&round).append(&step).append(&block_hash.unwrap_or_else(H256::zero));
s.append(&vote_step.height).append(&vote_step.round).append(&vote_step.step).append(&block_hash.unwrap_or_else(H256::zero));
s.out()
}
pub fn message_full_rlp(signature: &H520, vote_info: &Bytes) -> Bytes {
let mut s = RlpStream::new_list(2);
s.append(signature).append_raw(vote_info, 1);
@@ -199,14 +192,17 @@ mod tests {
use super::*;
use account_provider::AccountProvider;
use header::Header;
use ethkey::Secret;
#[test]
fn encode_decode() {
let message = ConsensusMessage {
signature: H520::default(),
height: 10,
round: 123,
step: Step::Precommit,
vote_step: VoteStep {
height: 10,
round: 123,
step: Step::Precommit,
},
block_hash: Some("1".sha3())
};
let raw_rlp = ::rlp::encode(&message).to_vec();
@@ -215,9 +211,11 @@ mod tests {
let message = ConsensusMessage {
signature: H520::default(),
height: 1314,
round: 0,
step: Step::Prevote,
vote_step: VoteStep {
height: 1314,
round: 0,
step: Step::Prevote,
},
block_hash: None
};
let raw_rlp = ::rlp::encode(&message);
@@ -228,10 +226,10 @@ mod tests {
#[test]
fn generate_and_verify() {
let tap = Arc::new(AccountProvider::transient_provider());
let addr = tap.insert_account("0".sha3(), "0").unwrap();
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
tap.unlock_account_permanently(addr, "0".into()).unwrap();
let mi = message_info_rlp(123, 2, Step::Precommit, Some(H256::default()));
let mi = message_info_rlp(&VoteStep::new(123, 2, Step::Precommit), Some(H256::default()));
let raw_rlp = message_full_rlp(&tap.sign(addr, None, mi.sha3()).unwrap().into(), &mi);
@@ -254,9 +252,11 @@ mod tests {
message,
ConsensusMessage {
signature: Default::default(),
height: 0,
round: 0,
step: Step::Propose,
vote_step: VoteStep {
height: 0,
round: 0,
step: Step::Propose,
},
block_hash: Some(header.bare_hash())
}
);
@@ -267,12 +267,10 @@ mod tests {
let header = Header::default();
let pro = ConsensusMessage {
signature: Default::default(),
height: 0,
round: 0,
step: Step::Propose,
vote_step: VoteStep::new(0, 0, Step::Propose),
block_hash: Some(header.bare_hash())
};
let pre = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
let pre = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash()));
assert_eq!(pro.precommit_hash(), pre.sha3());
}

View File

@@ -53,7 +53,7 @@ use self::transition::TransitionHandler;
use self::params::TendermintParams;
use self::vote_collector::VoteCollector;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Step {
Propose,
Prevote,
@@ -163,13 +163,13 @@ impl Tendermint {
let h = self.height.load(AtomicOrdering::SeqCst);
let r = self.round.load(AtomicOrdering::SeqCst);
let s = self.step.read();
let vote_info = message_info_rlp(h, r, *s, block_hash);
let vote_info = message_info_rlp(&VoteStep::new(h, r, *s), block_hash);
let authority = self.authority.read();
match ap.sign(*authority, self.password.read().clone(), vote_info.sha3()).map(Into::into) {
Ok(signature) => {
let message_rlp = message_full_rlp(&signature, &vote_info);
let message = ConsensusMessage::new(signature, h, r, *s, block_hash);
self.votes.vote(message.clone(), *authority);
self.votes.vote(message.clone(), &*authority);
debug!(target: "poa", "Generated {:?} as {}.", message, *authority);
self.handle_valid_message(&message);
@@ -221,7 +221,7 @@ impl Tendermint {
},
Step::Prevote => {
let block_hash = match *self.lock_change.read() {
Some(ref m) if !self.should_unlock(m.round) => m.block_hash,
Some(ref m) if !self.should_unlock(m.vote_step.round) => m.block_hash,
_ => self.proposal.read().clone(),
};
self.generate_and_broadcast_message(block_hash);
@@ -230,8 +230,8 @@ impl Tendermint {
trace!(target: "poa", "to_step: Precommit.");
let block_hash = match *self.lock_change.read() {
Some(ref m) if self.is_round(m) && m.block_hash.is_some() => {
trace!(target: "poa", "Setting last lock: {}", m.round);
self.last_lock.store(m.round, AtomicOrdering::SeqCst);
trace!(target: "poa", "Setting last lock: {}", m.vote_step.round);
self.last_lock.store(m.vote_step.round, AtomicOrdering::SeqCst);
m.block_hash
},
_ => None,
@@ -246,7 +246,7 @@ impl Tendermint {
if let Some(block_hash) = *self.proposal.read() {
// Generate seal and remove old votes.
if self.is_proposer(&*self.authority.read()).is_ok() {
if let Some(seal) = self.votes.seal_signatures(height, round, block_hash) {
if let Some(seal) = self.votes.seal_signatures(height, round, &block_hash) {
trace!(target: "poa", "Collected seal: {:?}", seal);
let seal = vec![
::rlp::encode(&round).to_vec(),
@@ -290,11 +290,11 @@ impl Tendermint {
}
fn is_height(&self, message: &ConsensusMessage) -> bool {
message.is_height(self.height.load(AtomicOrdering::SeqCst))
message.vote_step.is_height(self.height.load(AtomicOrdering::SeqCst))
}
fn is_round(&self, message: &ConsensusMessage) -> bool {
message.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst))
message.vote_step.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst))
}
fn increment_round(&self, n: Round) {
@@ -302,20 +302,20 @@ impl Tendermint {
self.round.fetch_add(n, AtomicOrdering::SeqCst);
}
fn should_unlock(&self, lock_change_round: Round) -> bool {
fn should_unlock(&self, lock_change_round: Round) -> bool {
self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_round
&& lock_change_round < self.round.load(AtomicOrdering::SeqCst)
}
fn has_enough_any_votes(&self) -> bool {
let step_votes = self.votes.count_step_votes(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read());
let step_votes = self.votes.count_step_votes(&VoteStep::new(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read()));
self.is_above_threshold(step_votes)
}
fn has_enough_future_step_votes(&self, message: &ConsensusMessage) -> bool {
if message.round > self.round.load(AtomicOrdering::SeqCst) {
let step_votes = self.votes.count_step_votes(message.height, message.round, message.step);
fn has_enough_future_step_votes(&self, vote_step: &VoteStep) -> bool {
if vote_step.round > self.round.load(AtomicOrdering::SeqCst) {
let step_votes = self.votes.count_step_votes(vote_step);
self.is_above_threshold(step_votes)
} else {
false
@@ -328,12 +328,13 @@ impl Tendermint {
}
fn handle_valid_message(&self, message: &ConsensusMessage) {
let ref vote_step = message.vote_step;
let is_newer_than_lock = match *self.lock_change.read() {
Some(ref lock) => message > lock,
Some(ref lock) => vote_step > &lock.vote_step,
None => true,
};
let lock_change = is_newer_than_lock
&& message.step == Step::Prevote
&& vote_step.step == Step::Prevote
&& message.block_hash.is_some()
&& self.has_enough_aligned_votes(message);
if lock_change {
@@ -351,15 +352,15 @@ impl Tendermint {
Some(Step::Commit)
}
},
Step::Precommit if self.has_enough_future_step_votes(message) => {
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
Step::Precommit if self.has_enough_future_step_votes(&vote_step) => {
self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst));
Some(Step::Precommit)
},
// Avoid counting twice.
Step::Prevote if lock_change => Some(Step::Precommit),
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
Step::Prevote if self.has_enough_future_step_votes(message) => {
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
Step::Prevote if self.has_enough_future_step_votes(&vote_step) => {
self.increment_round(vote_step.round - self.round.load(AtomicOrdering::SeqCst));
Some(Step::Prevote)
},
_ => None,
@@ -390,8 +391,8 @@ impl Engine for Tendermint {
let message = ConsensusMessage::new_proposal(header).expect("Invalid header.");
map![
"signature".into() => message.signature.to_string(),
"height".into() => message.height.to_string(),
"round".into() => message.round.to_string(),
"height".into() => message.vote_step.height.to_string(),
"round".into() => message.vote_step.round.to_string(),
"block_hash".into() => message.block_hash.as_ref().map(ToString::to_string).unwrap_or("".into())
]
}
@@ -431,11 +432,11 @@ impl Engine for Tendermint {
let height = header.number() as Height;
let round = self.round.load(AtomicOrdering::SeqCst);
let bh = Some(header.bare_hash());
let vote_info = message_info_rlp(height, round, Step::Propose, bh.clone());
let vote_info = message_info_rlp(&VoteStep::new(height, round, Step::Propose), bh.clone());
if let Ok(signature) = ap.sign(*author, self.password.read().clone(), vote_info.sha3()).map(H520::from) {
// Insert Propose vote.
debug!(target: "poa", "Submitting proposal {} at height {} round {}.", header.bare_hash(), height, round);
self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), *author);
self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), author);
// Remember proposal for later seal submission.
*self.proposal.write() = bh;
Seal::Proposal(vec![
@@ -461,9 +462,11 @@ impl Engine for Tendermint {
if !self.is_authority(&sender) {
Err(EngineError::NotAuthorized(sender))?;
}
self.broadcast_message(rlp.as_raw().to_vec());
if self.votes.vote(message.clone(), &sender).is_some() {
Err(EngineError::DoubleVote(sender))?
}
trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender);
self.votes.vote(message.clone(), sender);
self.broadcast_message(rlp.as_raw().to_vec());
self.handle_valid_message(&message);
}
Ok(())
@@ -502,7 +505,7 @@ impl Engine for Tendermint {
}
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
let proposal = ConsensusMessage::new_proposal(header)?;
let proposal = ConsensusMessage::new_proposal(header)?;
let proposer = proposal.verify()?;
if !self.is_authority(&proposer) {
Err(EngineError::NotAuthorized(proposer))?
@@ -541,7 +544,7 @@ impl Engine for Tendermint {
found: signatures_len
}))?;
}
self.is_round_proposer(proposal.height, proposal.round, &proposer)?;
self.is_round_proposer(proposal.vote_step.height, proposal.vote_step.round, &proposer)?;
}
Ok(())
}
@@ -607,16 +610,16 @@ impl Engine for Tendermint {
let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed");
if signatures_len != 1 {
// New Commit received, skip to next height.
trace!(target: "poa", "Received a commit for height {}, round {}.", proposal.height, proposal.round);
self.to_next_height(proposal.height);
trace!(target: "poa", "Received a commit: {:?}.", proposal.vote_step);
self.to_next_height(proposal.vote_step.height);
return false;
}
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
debug!(target: "poa", "Received a new proposal for height {}, round {} from {}.", proposal.height, proposal.round, proposer);
debug!(target: "poa", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
if self.is_round(&proposal) {
*self.proposal.write() = proposal.block_hash.clone();
}
self.votes.vote(proposal, proposer);
self.votes.vote(proposal, &proposer);
true
}
@@ -671,6 +674,7 @@ mod tests {
use error::{Error, BlockError};
use header::Header;
use env_info::EnvInfo;
use ethkey::Secret;
use client::chain_notify::ChainNotify;
use miner::MinerService;
use tests::helpers::*;
@@ -703,7 +707,7 @@ mod tests {
}
fn vote<F>(engine: &Engine, signer: F, height: usize, round: usize, step: Step, block_hash: Option<H256>) -> Bytes where F: FnOnce(H256) -> Result<H520, ::account_provider::Error> {
let mi = message_info_rlp(height, round, step, block_hash);
let mi = message_info_rlp(&VoteStep::new(height, round, step), block_hash);
let m = message_full_rlp(&signer(mi.sha3()).unwrap().into(), &mi);
engine.handle_message(&m).unwrap();
m
@@ -711,7 +715,7 @@ mod tests {
fn proposal_seal(tap: &Arc<AccountProvider>, header: &Header, round: Round) -> Vec<Bytes> {
let author = header.author();
let vote_info = message_info_rlp(header.number() as Height, round, Step::Propose, Some(header.bare_hash()));
let vote_info = message_info_rlp(&VoteStep::new(header.number() as Height, round, Step::Propose), Some(header.bare_hash()));
let signature = tap.sign(*author, None, vote_info.sha3()).unwrap();
vec![
::rlp::encode(&round).to_vec(),
@@ -721,7 +725,7 @@ mod tests {
}
fn insert_and_unlock(tap: &Arc<AccountProvider>, acc: &str) -> Address {
let addr = tap.insert_account(acc.sha3(), acc).unwrap();
let addr = tap.insert_account(Secret::from_slice(&acc.sha3()).unwrap(), acc).unwrap();
tap.unlock_account_permanently(addr, acc.into()).unwrap();
addr
}
@@ -825,7 +829,7 @@ mod tests {
header.set_author(proposer);
let mut seal = proposal_seal(&tap, &header, 0);
let vote_info = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
let vote_info = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash()));
let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap();
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec();
@@ -886,7 +890,7 @@ mod tests {
fn relays_messages() {
let (spec, tap) = setup();
let engine = spec.engine.clone();
let v0 = insert_and_register(&tap, engine.as_ref(), "0");
let v1 = insert_and_register(&tap, engine.as_ref(), "1");

View File

@@ -17,13 +17,50 @@
//! Collects votes on hashes at each height and round.
use util::*;
use super::message::ConsensusMessage;
use super::{Height, Round, Step};
use super::message::*;
use super::{Height, Round, Step, BlockHash};
#[derive(Debug)]
pub struct VoteCollector {
/// Storing all Proposals, Prevotes and Precommits.
votes: RwLock<BTreeMap<ConsensusMessage, Address>>,
votes: RwLock<BTreeMap<VoteStep, StepCollector>>,
}
#[derive(Debug, Default)]
struct StepCollector {
voted: HashSet<Address>,
pub block_votes: HashMap<Option<BlockHash>, HashMap<H520, Address>>,
messages: HashSet<ConsensusMessage>,
}
impl StepCollector {
/// Returns Some(&Address) when validator is double voting.
fn insert<'a>(&mut self, message: ConsensusMessage, address: &'a Address) -> Option<&'a Address> {
// Do nothing when message was seen.
if self.messages.insert(message.clone()) {
if self.voted.insert(address.clone()) {
self
.block_votes
.entry(message.block_hash)
.or_insert_with(HashMap::new)
.insert(message.signature, address.clone());
} else {
// Bad validator sent a different message.
return Some(address);
}
}
None
}
/// Count all votes for the given block hash at this step.
fn count_block(&self, block_hash: &Option<BlockHash>) -> usize {
self.block_votes.get(block_hash).map_or(0, HashMap::len)
}
/// Count all votes collected for the given step.
fn count(&self) -> usize {
self.block_votes.values().map(HashMap::len).sum()
}
}
#[derive(Debug)]
@@ -42,109 +79,105 @@ impl PartialEq for SealSignatures {
impl Eq for SealSignatures {}
impl VoteCollector {
pub fn new() -> VoteCollector {
pub fn new() -> Self {
let mut collector = BTreeMap::new();
// Insert dummy message to fulfill invariant: "only messages newer than the oldest are inserted".
collector.insert(ConsensusMessage {
signature: H520::default(),
height: 0,
round: 0,
step: Step::Propose,
block_hash: None
},
Address::default());
// Insert dummy entry to fulfill invariant: "only messages newer than the oldest are inserted".
collector.insert(VoteStep::new(0, 0, Step::Propose), Default::default());
VoteCollector { votes: RwLock::new(collector) }
}
/// Insert vote if it is newer than the oldest one.
pub fn vote(&self, message: ConsensusMessage, voter: Address) -> Option<Address> {
self.votes.write().insert(message, voter)
pub fn vote<'a>(&self, message: ConsensusMessage, voter: &'a Address) -> Option<&'a Address> {
self
.votes
.write()
.entry(message.vote_step.clone())
.or_insert_with(Default::default)
.insert(message, voter)
}
/// Checks if the message should be ignored.
pub fn is_old_or_known(&self, message: &ConsensusMessage) -> bool {
self.votes.read().get(message).map_or(false, |a| {
trace!(target: "poa", "Known message from {}: {:?}.", a, message);
true
}) || {
self
.votes
.read()
.get(&message.vote_step)
.map_or(false, |c| {
let is_known = c.messages.contains(message);
if is_known { trace!(target: "poa", "Known message: {:?}.", message); }
is_known
})
|| {
let guard = self.votes.read();
let is_old = guard.keys().next().map_or(true, |oldest| message <= oldest);
let is_old = guard.keys().next().map_or(true, |oldest| message.vote_step <= *oldest);
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
is_old
}
}
/// Throws out messages older than message, leaves message as marker for the oldest.
pub fn throw_out_old(&self, message: &ConsensusMessage) {
pub fn throw_out_old(&self, vote_step: &VoteStep) {
let mut guard = self.votes.write();
let new_collector = guard.split_off(message);
let new_collector = guard.split_off(vote_step);
*guard = new_collector;
}
pub fn seal_signatures(&self, height: Height, round: Round, block_hash: H256) -> Option<SealSignatures> {
let bh = Some(block_hash);
let (proposal, votes) = {
/// Collects the signatures used to seal a block.
pub fn seal_signatures(&self, height: Height, round: Round, block_hash: &H256) -> Option<SealSignatures> {
let ref bh = Some(*block_hash);
let precommit_step = VoteStep::new(height, round, Step::Precommit);
let maybe_seal = {
let guard = self.votes.read();
let mut current_signatures = guard.keys().skip_while(|m| !m.is_block_hash(height, round, Step::Propose, bh));
let proposal = current_signatures.next().cloned();
let votes = current_signatures
.skip_while(|m| !m.is_block_hash(height, round, Step::Precommit, bh))
.filter(|m| m.is_block_hash(height, round, Step::Precommit, bh))
.cloned()
.collect::<Vec<_>>();
(proposal, votes)
guard
.get(&VoteStep::new(height, round, Step::Propose))
.and_then(|c| c.block_votes.get(bh))
.and_then(|proposals| proposals.keys().next())
.map(|proposal| SealSignatures {
proposal: proposal.clone(),
votes: guard
.get(&precommit_step)
.and_then(|c| c.block_votes.get(bh))
.map(|precommits| precommits.keys().cloned().collect())
.unwrap_or_else(Vec::new),
})
.and_then(|seal| if seal.votes.is_empty() { None } else { Some(seal) })
};
if votes.is_empty() {
return None;
if maybe_seal.is_some() {
// Remove messages that are no longer relevant.
self.throw_out_old(&precommit_step);
}
// Remove messages that are no longer relevant.
votes.last().map(|m| self.throw_out_old(m));
let mut votes_vec: Vec<_> = votes.into_iter().map(|m| m.signature).collect();
votes_vec.sort();
proposal.map(|p| SealSignatures {
proposal: p.signature,
votes: votes_vec,
})
maybe_seal
}
/// Count votes which agree with the given message.
pub fn count_aligned_votes(&self, message: &ConsensusMessage) -> usize {
let guard = self.votes.read();
guard.keys()
.skip_while(|m| !m.is_aligned(message))
// sorted by signature so might not be continuous
.filter(|m| m.is_aligned(message))
.count()
self
.votes
.read()
.get(&message.vote_step)
.map_or(0, |m| m.count_block(&message.block_hash))
}
pub fn count_step_votes(&self, height: Height, round: Round, step: Step) -> usize {
let guard = self.votes.read();
let current = guard.iter().skip_while(|&(m, _)| !m.is_step(height, round, step));
let mut origins = HashSet::new();
let mut n = 0;
for (message, origin) in current {
if message.is_step(height, round, step) {
if origins.insert(origin) {
n += 1;
} else {
warn!("count_step_votes: Authority {} has cast multiple step votes, this indicates malicious behaviour.", origin)
}
}
}
n
/// Count all votes collected for a given step.
pub fn count_step_votes(&self, vote_step: &VoteStep) -> usize {
self.votes.read().get(vote_step).map_or(0, StepCollector::count)
}
/// Get all messages older than the height.
pub fn get_up_to(&self, height: Height) -> Vec<Bytes> {
let guard = self.votes.read();
guard
.keys()
.filter(|m| m.step.is_pre())
.take_while(|m| m.height <= height)
.map(|m| ::rlp::encode(m).to_vec())
.collect()
.iter()
.filter(|&(s, _)| s.step.is_pre())
.take_while(|&(s, _)| s.height <= height)
.map(|(_, c)| c.messages.iter().map(|m| ::rlp::encode(m).to_vec()).collect::<Vec<_>>())
.fold(Vec::new(), |mut acc, mut messages| { acc.append(&mut messages); acc })
}
/// Retrieve address from which the message was sent from cache.
pub fn get(&self, message: &ConsensusMessage) -> Option<Address> {
let guard = self.votes.read();
guard.get(message).cloned()
guard.get(&message.vote_step).and_then(|c| c.block_votes.get(&message.block_hash)).and_then(|origins| origins.get(&message.signature).cloned())
}
}
@@ -152,15 +185,15 @@ impl VoteCollector {
mod tests {
use util::*;
use super::*;
use super::super::{Height, Round, BlockHash, Step};
use super::super::message::ConsensusMessage;
use super::super::{BlockHash, Step};
use super::super::message::*;
fn random_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>) -> Option<H160> {
full_vote(collector, signature, h, r, step, block_hash, H160::random())
fn random_vote(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option<BlockHash>) -> bool {
full_vote(collector, signature, vote_step, block_hash, &H160::random()).is_none()
}
fn full_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>, address: Address) -> Option<H160> {
collector.vote(ConsensusMessage { signature: signature, height: h, round: r, step: step, block_hash: block_hash }, address)
fn full_vote<'a>(collector: &VoteCollector, signature: H520, vote_step: VoteStep, block_hash: Option<BlockHash>, address: &'a Address) -> Option<&'a Address> {
collector.vote(ConsensusMessage { signature: signature, vote_step: vote_step, block_hash: block_hash }, address)
}
#[test]
@@ -173,68 +206,71 @@ mod tests {
for _ in 0..5 {
signatures.push(H520::random());
}
let propose_step = VoteStep::new(h, r, Step::Propose);
let prevote_step = VoteStep::new(h, r, Step::Prevote);
let precommit_step = VoteStep::new(h, r, Step::Precommit);
// Wrong height proposal.
random_vote(&collector, signatures[4].clone(), h - 1, r, Step::Propose, bh.clone());
random_vote(&collector, signatures[4].clone(), VoteStep::new(h - 1, r, Step::Propose), bh.clone());
// Good proposal
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, bh.clone());
random_vote(&collector, signatures[0].clone(), propose_step.clone(), bh.clone());
// Wrong block proposal.
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, Some("0".sha3()));
random_vote(&collector, signatures[0].clone(), propose_step.clone(), Some("0".sha3()));
// Wrong block precommit.
random_vote(&collector, signatures[3].clone(), h, r, Step::Precommit, Some("0".sha3()));
random_vote(&collector, signatures[3].clone(), precommit_step.clone(), Some("0".sha3()));
// Wrong round proposal.
random_vote(&collector, signatures[0].clone(), h, r - 1, Step::Propose, bh.clone());
random_vote(&collector, signatures[0].clone(), VoteStep::new(h, r - 1, Step::Propose), bh.clone());
// Prevote.
random_vote(&collector, signatures[0].clone(), h, r, Step::Prevote, bh.clone());
random_vote(&collector, signatures[0].clone(), prevote_step.clone(), bh.clone());
// Relevant precommit.
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone());
// Replcated vote.
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
random_vote(&collector, signatures[2].clone(), precommit_step.clone(), bh.clone());
// Wrong round precommit.
random_vote(&collector, signatures[4].clone(), h, r + 1, Step::Precommit, bh.clone());
random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone());
// Wrong height precommit.
random_vote(&collector, signatures[3].clone(), h + 1, r, Step::Precommit, bh.clone());
random_vote(&collector, signatures[3].clone(), VoteStep::new(h + 1, r, Step::Precommit), bh.clone());
// Relevant precommit.
random_vote(&collector, signatures[1].clone(), h, r, Step::Precommit, bh.clone());
random_vote(&collector, signatures[1].clone(), precommit_step.clone(), bh.clone());
// Wrong round precommit, same signature.
random_vote(&collector, signatures[1].clone(), h, r + 1, Step::Precommit, bh.clone());
random_vote(&collector, signatures[1].clone(), VoteStep::new(h, r + 1, Step::Precommit), bh.clone());
// Wrong round precommit.
random_vote(&collector, signatures[4].clone(), h, r - 1, Step::Precommit, bh.clone());
random_vote(&collector, signatures[4].clone(), VoteStep::new(h, r - 1, Step::Precommit), bh.clone());
let seal = SealSignatures {
proposal: signatures[0],
votes: signatures[1..3].to_vec()
};
assert_eq!(seal, collector.seal_signatures(h, r, bh.unwrap()).unwrap());
assert_eq!(seal, collector.seal_signatures(h, r, &bh.unwrap()).unwrap());
}
#[test]
fn count_votes() {
let collector = VoteCollector::new();
let prevote_step = VoteStep::new(3, 2, Step::Prevote);
let precommit_step = VoteStep::new(3, 2, Step::Precommit);
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3()));
random_vote(&collector, H520::random(), VoteStep::new(3, 1, Step::Prevote), Some("0".sha3()));
// good precommit
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
random_vote(&collector, H520::random(), precommit_step.clone(), Some("0".sha3()));
random_vote(&collector, H520::random(), VoteStep::new(3, 3, Step::Precommit), Some("0".sha3()));
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, H520::random(), prevote_step.clone(), Some("1".sha3()));
// good prevote
let same_sig = H520::random();
random_vote(&collector, same_sig.clone(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, same_sig, 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, same_sig.clone(), prevote_step.clone(), Some("1".sha3()));
random_vote(&collector, same_sig, prevote_step.clone(), Some("1".sha3()));
// good precommit
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("1".sha3()));
random_vote(&collector, H520::random(), precommit_step.clone(), Some("1".sha3()));
// good prevote
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
random_vote(&collector, H520::random(), prevote_step.clone(), Some("0".sha3()));
random_vote(&collector, H520::random(), VoteStep::new(2, 2, Step::Precommit), Some("2".sha3()));
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 4);
assert_eq!(collector.count_step_votes(3, 2, Step::Precommit), 2);
assert_eq!(collector.count_step_votes(&prevote_step), 4);
assert_eq!(collector.count_step_votes(&precommit_step), 2);
let message = ConsensusMessage {
signature: H520::default(),
height: 3,
round: 2,
step: Step::Prevote,
vote_step: prevote_step,
block_hash: Some("1".sha3())
};
assert_eq!(collector.count_aligned_votes(&message), 2);
@@ -243,30 +279,29 @@ mod tests {
#[test]
fn remove_old() {
let collector = VoteCollector::new();
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
let message = ConsensusMessage {
signature: H520::default(),
height: 3,
round: 2,
step: Step::Precommit,
block_hash: Some("1".sha3())
let vote = |height, round, step, hash| {
random_vote(&collector, H520::random(), VoteStep::new(height, round, step), hash);
};
collector.throw_out_old(&message);
vote(3, 2, Step::Prevote, Some("0".sha3()));
vote(3, 1, Step::Prevote, Some("0".sha3()));
vote(3, 3, Step::Precommit, Some("0".sha3()));
vote(3, 2, Step::Prevote, Some("1".sha3()));
vote(3, 2, Step::Prevote, Some("1".sha3()));
vote(3, 2, Step::Prevote, Some("0".sha3()));
vote(2, 2, Step::Precommit, Some("2".sha3()));
collector.throw_out_old(&VoteStep::new(3, 2, Step::Precommit));
assert_eq!(collector.votes.read().len(), 1);
}
#[test]
fn malicious_authority() {
let collector = VoteCollector::new();
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()), Address::default());
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()), Address::default());
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 1);
let vote_step = VoteStep::new(3, 2, Step::Prevote);
// Vote is inserted fine.
assert!(full_vote(&collector, H520::random(), vote_step.clone(), Some("0".sha3()), &Address::default()).is_none());
// Returns the double voting address.
full_vote(&collector, H520::random(), vote_step.clone(), Some("1".sha3()), &Address::default()).unwrap();
assert_eq!(collector.count_step_votes(&vote_step), 1);
}
}

View File

@@ -118,17 +118,17 @@ mod provider {
}
}
fn as_string<T: fmt::Debug>(e: T) -> String { format!("{:?}", e) }
/// Auto-generated from: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}`
#[allow(dead_code)]
pub fn get_validators(&self) -> Result<Vec<util::Address>, String> {
pub fn get_validators(&self) -> Result<Vec<util::Address>, String> {
let call = self.contract.function("getValidators".into()).map_err(Self::as_string)?;
let data = call.encode_call(
vec![]
).map_err(Self::as_string)?;
let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?;
let mut result = output.into_iter().rev().collect::<Vec<_>>();
Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::<Option<Vec<[u8; 20]>>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::<Vec<_>>() }))
Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::<Option<Vec<[u8; 20]>>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::<Vec<_>>() }))
}
}
}
@@ -140,6 +140,7 @@ mod tests {
use account_provider::AccountProvider;
use transaction::{Transaction, Action};
use client::{BlockChainClient, EngineClient};
use ethkey::Secret;
use miner::MinerService;
use tests::helpers::generate_dummy_client_with_spec_and_data;
use super::super::ValidatorSet;
@@ -158,8 +159,9 @@ mod tests {
#[test]
fn changes_validators() {
let tap = Arc::new(AccountProvider::transient_provider());
let v0 = tap.insert_account("1".sha3(), "").unwrap();
let v1 = tap.insert_account("0".sha3(), "").unwrap();
let s0 = Secret::from_slice(&"1".sha3()).unwrap();
let v0 = tap.insert_account(s0.clone(), "").unwrap();
let v1 = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "").unwrap();
let spec_factory = || {
let spec = Spec::new_validator_contract();
spec.engine.register_account_provider(tap.clone());
@@ -178,7 +180,7 @@ mod tests {
action: Action::Call(validator_contract),
value: 0.into(),
data: "f94e18670000000000000000000000000000000000000000000000000000000000000001".from_hex().unwrap(),
}.sign(&"1".sha3(), None);
}.sign(&s0, None);
client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap();
client.update_sealing();
assert_eq!(client.chain_info().best_block_number, 1);
@@ -190,7 +192,7 @@ mod tests {
action: Action::Call(validator_contract),
value: 0.into(),
data: "4d238c8e00000000000000000000000082a978b3f5962a5b0957d9ee9eef472ee55b42f1".from_hex().unwrap(),
}.sign(&"1".sha3(), None);
}.sign(&s0, None);
client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap();
client.update_sealing();
// The transaction is not yet included so still unable to seal.
@@ -209,7 +211,7 @@ mod tests {
action: Action::Call(Address::default()),
value: 0.into(),
data: Vec::new(),
}.sign(&"1".sha3(), None);
}.sign(&s0, None);
client.miner().import_own_transaction(client.as_ref(), tx.into()).unwrap();
client.update_sealing();
// Able to seal again.

View File

@@ -31,6 +31,9 @@ use ethjson;
use rlp::{self, UntrustedRlp, View};
use blockchain::extras::BlockDetails;
/// Parity tries to round block.gas_limit to multiple of this constant
pub const PARITY_GAS_LIMIT_DETERMINANT: U256 = U256([37, 0, 0, 0]);
/// Ethash params.
#[derive(Debug, PartialEq)]
pub struct EthashParams {
@@ -180,14 +183,23 @@ impl Engine for Ethash {
let bound_divisor = self.ethash_params.gas_limit_bound_divisor;
let lower_limit = gas_limit - gas_limit / bound_divisor + 1.into();
let upper_limit = gas_limit + gas_limit / bound_divisor - 1.into();
if gas_limit < gas_floor_target {
min(gas_floor_target, upper_limit)
let gas_limit = if gas_limit < gas_floor_target {
let gas_limit = min(gas_floor_target, upper_limit);
round_block_gas_limit(gas_limit, lower_limit, upper_limit)
} else if gas_limit > gas_ceil_target {
max(gas_ceil_target, lower_limit)
let gas_limit = max(gas_ceil_target, lower_limit);
round_block_gas_limit(gas_limit, lower_limit, upper_limit)
} else {
max(gas_floor_target, min(min(gas_ceil_target, upper_limit),
lower_limit + (header.gas_used().clone() * 6.into() / 5.into()) / bound_divisor))
}
let total_lower_limit = max(lower_limit, gas_floor_target);
let total_upper_limit = min(upper_limit, gas_ceil_target);
let gas_limit = max(gas_floor_target, min(total_upper_limit,
lower_limit + (header.gas_used().clone() * 6.into() / 5.into()) / bound_divisor));
round_block_gas_limit(gas_limit, total_lower_limit, total_upper_limit)
};
// ensure that we are not violating protocol limits
debug_assert!(gas_limit >= lower_limit);
debug_assert!(gas_limit <= upper_limit);
gas_limit
};
header.set_difficulty(difficulty);
header.set_gas_limit(gas_limit);
@@ -338,6 +350,23 @@ pub fn is_new_best_block(best_total_difficulty: U256, parent_details: &BlockDeta
parent_details.total_difficulty + new_header.difficulty() > best_total_difficulty
}
// Try to round gas_limit a bit so that:
// 1) it will still be in desired range
// 2) it will be a nearest (with tendency to increase) multiple of PARITY_GAS_LIMIT_DETERMINANT
fn round_block_gas_limit(gas_limit: U256, lower_limit: U256, upper_limit: U256) -> U256 {
let increased_gas_limit = gas_limit + (PARITY_GAS_LIMIT_DETERMINANT - gas_limit % PARITY_GAS_LIMIT_DETERMINANT);
if increased_gas_limit > upper_limit {
let decreased_gas_limit = increased_gas_limit - PARITY_GAS_LIMIT_DETERMINANT;
if decreased_gas_limit < lower_limit {
gas_limit
} else {
decreased_gas_limit
}
} else {
increased_gas_limit
}
}
#[cfg_attr(feature="dev", allow(wrong_self_convention))]
impl Ethash {
fn calculate_difficulty(&self, header: &Header, parent: &Header) -> U256 {
@@ -435,11 +464,12 @@ mod tests {
use util::*;
use block::*;
use tests::helpers::*;
use engines::Engine;
use env_info::EnvInfo;
use error::{BlockError, Error};
use header::Header;
use super::super::{new_morden, new_homestead_test};
use super::{Ethash, EthashParams};
use super::{Ethash, EthashParams, PARITY_GAS_LIMIT_DETERMINANT};
use rlp;
#[test]
@@ -777,4 +807,47 @@ mod tests {
ethash.calculate_difficulty(&header, &parent_header)
);
}
#[test]
fn gas_limit_is_multiple_of_determinant() {
let spec = new_homestead_test();
let ethash = Ethash::new(spec.params, get_default_ethash_params(), BTreeMap::new());
let mut parent = Header::new();
let mut header = Header::new();
header.set_number(1);
// this test will work for this constant only
assert_eq!(PARITY_GAS_LIMIT_DETERMINANT, U256::from(37));
// when parent.gas_limit < gas_floor_target:
parent.set_gas_limit(U256::from(50_000));
ethash.populate_from_parent(&mut header, &parent, U256::from(100_000), U256::from(200_000));
assert_eq!(*header.gas_limit(), U256::from(50_024));
// when parent.gas_limit > gas_ceil_target:
parent.set_gas_limit(U256::from(250_000));
ethash.populate_from_parent(&mut header, &parent, U256::from(100_000), U256::from(200_000));
assert_eq!(*header.gas_limit(), U256::from(249_787));
// when parent.gas_limit is in miner's range
header.set_gas_used(U256::from(150_000));
parent.set_gas_limit(U256::from(150_000));
ethash.populate_from_parent(&mut header, &parent, U256::from(100_000), U256::from(200_000));
assert_eq!(*header.gas_limit(), U256::from(150_035));
// when parent.gas_limit is in miner's range
// && we can NOT increase it to be multiple of constant
header.set_gas_used(U256::from(150_000));
parent.set_gas_limit(U256::from(150_000));
ethash.populate_from_parent(&mut header, &parent, U256::from(100_000), U256::from(150_002));
assert_eq!(*header.gas_limit(), U256::from(149_998));
// when parent.gas_limit is in miner's range
// && we can NOT increase it to be multiple of constant
// && we can NOT decrease it to be multiple of constant
header.set_gas_used(U256::from(150_000));
parent.set_gas_limit(U256::from(150_000));
ethash.populate_from_parent(&mut header, &parent, U256::from(150_000), U256::from(150_002));
assert_eq!(*header.gas_limit(), U256::from(150_002));
}
}

View File

@@ -22,7 +22,7 @@ use action_params::ActionParams;
use evm::Ext;
/// Evm errors.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Error {
/// `OutOfGas` is returned when transaction execution runs out of gas.
/// The state should be reverted to the state from before the

View File

@@ -45,7 +45,7 @@ pub fn contract_address(address: &Address, nonce: &U256) -> Address {
}
/// Transaction execution options.
#[derive(Default)]
#[derive(Default, Copy, Clone, PartialEq)]
pub struct TransactOptions {
/// Enable call tracing.
pub tracing: bool,
@@ -463,8 +463,9 @@ impl<'a> Executive<'a> {
match result {
Err(evm::Error::Internal) => Err(ExecutionError::Internal),
Err(_) => {
Err(exception) => {
Ok(Executed {
exception: Some(exception),
gas: t.gas,
gas_used: t.gas,
refunded: U256::zero(),
@@ -479,6 +480,7 @@ impl<'a> Executive<'a> {
},
_ => {
Ok(Executed {
exception: None,
gas: t.gas,
gas_used: gas_used,
refunded: refunded,

View File

@@ -149,7 +149,8 @@ impl GasPriceCalibrator {
if Instant::now() >= self.next_calibration {
let usd_per_tx = self.options.usd_per_tx;
trace!(target: "miner", "Getting price info");
let price_info = PriceInfo::get(move |price: PriceInfo| {
PriceInfo::get(move |price: PriceInfo| {
trace!(target: "miner", "Price info arrived: {:?}", price);
let usd_per_eth = price.ethusd;
let wei_per_usd: f32 = 1.0e18 / usd_per_eth;
@@ -159,11 +160,7 @@ impl GasPriceCalibrator {
set_price(U256::from(wei_per_gas as u64));
});
if price_info.is_ok() {
self.next_calibration = Instant::now() + self.options.recalibration_period;
} else {
warn!(target: "miner", "Unable to update Ether price.");
}
self.next_calibration = Instant::now() + self.options.recalibration_period;
}
}
}
@@ -307,16 +304,6 @@ impl Miner {
#[cfg_attr(feature="dev", allow(match_same_arms))]
/// Prepares new block for sealing including top transactions from queue.
fn prepare_block(&self, chain: &MiningBlockChainClient) -> (ClosedBlock, Option<H256>) {
{
trace!(target: "miner", "prepare_block: recalibrating...");
let txq = self.transaction_queue.clone();
self.gas_pricer.lock().recalibrate(move |price| {
trace!(target: "miner", "prepare_block: Got gas price! {}", price);
txq.lock().set_minimal_gas_price(price);
});
trace!(target: "miner", "prepare_block: done recalibration.");
}
let _timer = PerfTimer::new("prepare_block");
let chain_info = chain.chain_info();
let (transactions, mut open_block, original_work_hash) = {
@@ -431,6 +418,16 @@ impl Miner {
(block, original_work_hash)
}
/// Asynchronously updates minimal gas price for transaction queue
pub fn recalibrate_minimal_gas_price(&self) {
debug!(target: "miner", "minimal_gas_price: recalibrating...");
let txq = self.transaction_queue.clone();
self.gas_pricer.lock().recalibrate(move |price| {
debug!(target: "miner", "minimal_gas_price: Got gas price! {}", price);
txq.lock().set_minimal_gas_price(price);
});
}
/// Check is reseal is allowed and necessary.
fn requires_reseal(&self, best_block: BlockNumber) -> bool {
let has_local_transactions = self.transaction_queue.lock().has_local_pending_transactions();
@@ -673,7 +670,7 @@ impl MinerService for Miner {
}
}
fn call(&self, chain: &MiningBlockChainClient, t: &SignedTransaction, analytics: CallAnalytics) -> Result<Executed, CallError> {
fn call(&self, client: &MiningBlockChainClient, t: &SignedTransaction, analytics: CallAnalytics) -> Result<Executed, CallError> {
let sealing_work = self.sealing_work.lock();
match sealing_work.queue.peek_last_ref() {
Some(work) => {
@@ -681,7 +678,7 @@ impl MinerService for Miner {
// TODO: merge this code with client.rs's fn call somwhow.
let header = block.header();
let last_hashes = Arc::new(chain.last_hashes());
let last_hashes = Arc::new(client.last_hashes());
let env_info = EnvInfo {
number: header.number(),
author: *header.author(),
@@ -706,16 +703,14 @@ impl MinerService for Miner {
state.add_balance(&sender, &(needed_balance - balance), CleanupMode::NoEmpty);
}
let options = TransactOptions { tracing: analytics.transaction_tracing, vm_tracing: analytics.vm_tracing, check_nonce: false };
let mut ret = Executive::new(&mut state, &env_info, &*self.engine, chain.vm_factory()).transact(t, options)?;
let mut ret = Executive::new(&mut state, &env_info, &*self.engine, client.vm_factory()).transact(t, options)?;
// TODO gav move this into Executive.
ret.state_diff = original_state.map(|original| state.diff_from(original));
Ok(ret)
},
None => {
chain.call(t, BlockId::Latest, analytics)
}
None => client.call(t, BlockId::Latest, analytics)
}
}
@@ -1126,6 +1121,9 @@ impl MinerService for Miner {
// First update gas limit in transaction queue
self.update_gas_limit(chain);
// Update minimal gas price
self.recalibrate_minimal_gas_price();
// Then import all transactions...
{
retracted.par_iter()

View File

@@ -21,7 +21,7 @@ use std::time::Duration;
use std::str::FromStr;
use std::sync::mpsc;
use hyper::client::{Handler, Request, Response, Client};
use hyper::{Next, Encoder, Decoder};
use hyper::{Url, Next, Encoder, Decoder};
use hyper::net::HttpStream;
#[derive(Debug)]
@@ -29,12 +29,12 @@ pub struct PriceInfo {
pub ethusd: f32,
}
pub struct SetPriceHandler<F: Fn(PriceInfo) + Sync + Send + 'static> {
pub struct SetPriceHandler<F> {
set_price: F,
channel: mpsc::Sender<()>,
}
impl<F: Fn(PriceInfo) + Sync + Send + 'static> Drop for SetPriceHandler<F> {
impl<F> Drop for SetPriceHandler<F> {
fn drop(&mut self) {
let _ = self.channel.send(());
}
@@ -47,42 +47,66 @@ impl<F: Fn(PriceInfo) + Sync + Send + 'static> Handler<HttpStream> for SetPriceH
fn on_response_readable(&mut self, r: &mut Decoder<HttpStream>) -> Next {
let mut body = String::new();
let _ = r.read_to_string(&mut body).ok()
.and_then(|_| Json::from_str(&body).ok())
.and_then(|json| json.find_path(&["result", "ethusd"])
.and_then(|obj| match *obj {
Json::String(ref s) => Some((self.set_price)(PriceInfo {
ethusd: FromStr::from_str(s)
.expect("Etherscan API will always return properly formatted price; qed")
})),
_ => None,
}));
let info = r.read_to_string(&mut body)
.map_err(|e| format!("Unable to read response: {:?}", e))
.and_then(|_| self.process_response(&body));
if let Err(e) = info {
warn!("Failed to auto-update latest ETH price: {:?}", e);
}
Next::end()
}
}
impl PriceInfo {
pub fn get<F: Fn(PriceInfo) + Sync + Send + 'static>(set_price: F) -> Result<(), ()> {
// TODO: Handle each error type properly
let client = Client::new().map_err(|_| ())?;
thread::spawn(move || {
let (tx, rx) = mpsc::channel();
let url = FromStr::from_str("http://api.etherscan.io/api?module=stats&action=ethprice")
.expect("string known to be a valid URL; qed");
let _ = client.request(
url,
SetPriceHandler {
set_price: set_price,
channel: tx,
}).ok().and_then(|_| rx.recv().ok());
client.close();
impl<F: Fn(PriceInfo) + Sync + Send + 'static> SetPriceHandler<F> {
fn process_response(&self, body: &str) -> Result<(), String> {
let json = Json::from_str(body).map_err(|e| format!("Invalid JSON returned: {:?}", e))?;
let obj = json.find_path(&["result", "ethusd"]).ok_or("USD price not found".to_owned())?;
let ethusd = match *obj {
Json::String(ref s) => FromStr::from_str(s).ok(),
_ => None,
}.ok_or("Unexpected price format.".to_owned())?;
(self.set_price)(PriceInfo {
ethusd: ethusd,
});
Ok(())
}
}
#[test]
impl PriceInfo {
pub fn get<F: Fn(PriceInfo) + Sync + Send + 'static>(set_price: F) {
thread::spawn(move || {
let url = FromStr::from_str("http://api.etherscan.io/api?module=stats&action=ethprice")
.expect("string known to be a valid URL; qed");
if let Err(e) = Self::request(url, set_price) {
warn!("Failed to auto-update latest ETH price: {:?}", e);
}
});
}
fn request<F: Fn(PriceInfo) + Send + Sync + 'static>(url: Url, set_price: F) -> Result<(), String> {
let (tx, rx) = mpsc::channel();
let client = Client::new().map_err(|e| format!("Unable to start client: {:?}", e))?;
client.request(
url,
SetPriceHandler {
set_price: set_price,
channel: tx,
},
).map_err(|_| "Request failed.".to_owned())?;
// Wait for exit
let _ = rx.recv().map_err(|e| format!("Request interrupted: {:?}", e))?;
client.close();
Ok(())
}
}
#[test] #[ignore]
fn should_get_price_info() {
use std::sync::Arc;
use std::time::Duration;
@@ -93,7 +117,7 @@ fn should_get_price_info() {
let done = Arc::new((Mutex::new(PriceInfo { ethusd: 0f32 }), Condvar::new()));
let rdone = done.clone();
PriceInfo::get(move |price| { let mut p = rdone.0.lock(); *p = price; rdone.1.notify_one(); }).unwrap();
PriceInfo::get(move |price| { let mut p = rdone.0.lock(); *p = price; rdone.1.notify_one(); });
let mut p = done.0.lock();
let t = done.1.wait_for(&mut p, Duration::from_millis(10000));
assert!(!t.timed_out());

View File

@@ -844,6 +844,7 @@ mod tests {
use std::str::FromStr;
use rustc_serialize::hex::FromHex;
use super::*;
use ethkey::Secret;
use util::{U256, H256, FixedHash, Address, Hashable};
use tests::helpers::*;
use devtools::*;
@@ -854,6 +855,10 @@ mod tests {
use trace::{FlatTrace, TraceError, trace};
use types::executed::CallType;
fn secret() -> Secret {
Secret::from_slice(&"".sha3()).unwrap()
}
#[test]
fn should_apply_create_transaction() {
init_log();
@@ -872,7 +877,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: FromHex::from_hex("601080600c6000396000f3006000355415600957005b60203560003555").unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
let result = state.apply(&info, &engine, &t, true).unwrap();
@@ -932,7 +937,7 @@ mod tests {
action: Action::Create,
value: 100.into(),
data: FromHex::from_hex("5b600056").unwrap(),
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
let result = state.apply(&info, &engine, &t, true).unwrap();
@@ -969,7 +974,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("6000").unwrap());
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
@@ -1012,7 +1017,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
let result = state.apply(&info, &engine, &t, true).unwrap();
@@ -1054,7 +1059,7 @@ mod tests {
action: Action::Call(0x1.into()),
value: 0.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
let result = state.apply(&info, engine, &t, true).unwrap();
@@ -1096,7 +1101,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 0.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("600060006000600060006001610be0f1").unwrap());
let result = state.apply(&info, engine, &t, true).unwrap();
@@ -1139,7 +1144,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 0.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b611000f2").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap());
@@ -1201,7 +1206,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 0.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("6000600060006000600b618000f4").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap());
@@ -1260,7 +1265,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("5b600056").unwrap());
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
@@ -1300,7 +1305,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("6000").unwrap());
@@ -1360,7 +1365,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006045600b6000f1").unwrap());
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
@@ -1415,7 +1420,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("600060006000600060ff600b6000f1").unwrap()); // not enough funds.
state.add_balance(t.sender().as_ref().unwrap(), &(100.into()), CleanupMode::NoEmpty);
@@ -1458,7 +1463,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],//600480600b6000396000f35b600056
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("5b600056").unwrap());
@@ -1514,7 +1519,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("60006000600060006000600c602b5a03f1").unwrap());
@@ -1589,7 +1594,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],//600480600b6000396000f35b600056
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("60006000600060006000600b602b5a03f1").unwrap());
state.init_code(&0xb.into(), FromHex::from_hex("60006000600060006000600c602b5a03f1505b601256").unwrap());
@@ -1662,7 +1667,7 @@ mod tests {
action: Action::Call(0xa.into()),
value: 100.into(),
data: vec![],
}.sign(&"".sha3(), None);
}.sign(&secret(), None);
state.init_code(&0xa.into(), FromHex::from_hex("73000000000000000000000000000000000000000bff").unwrap());
state.add_balance(&0xa.into(), &50.into(), CleanupMode::NoEmpty);

View File

@@ -28,7 +28,7 @@ use rlp::View;
use spec::Spec;
use views::BlockView;
use util::stats::Histogram;
use ethkey::KeyPair;
use ethkey::{KeyPair, Secret};
use transaction::{PendingTransaction, Transaction, Action};
use miner::MinerService;
@@ -290,7 +290,7 @@ fn change_history_size() {
#[test]
fn does_not_propagate_delayed_transactions() {
let key = KeyPair::from_secret("test".sha3()).unwrap();
let key = KeyPair::from_secret(Secret::from_slice(&"test".sha3()).unwrap()).unwrap();
let secret = key.secret();
let tx0 = PendingTransaction::new(Transaction {
nonce: 0.into(),

View File

@@ -163,7 +163,7 @@ pub fn generate_dummy_client_with_spec_and_data<F>(get_test_spec: F, block_numbe
let mut last_hashes = vec![];
let mut last_header = genesis_header.clone();
let kp = KeyPair::from_secret("".sha3()).unwrap();
let kp = KeyPair::from_secret_slice(&"".sha3()).unwrap();
let author = kp.address();
let mut n = 0;

View File

@@ -18,6 +18,7 @@
use util::{Bytes, U256, Address, U512};
use rlp::*;
use evm;
use trace::{VMTrace, FlatTrace};
use types::log_entry::LogEntry;
use types::state_diff::StateDiff;
@@ -65,6 +66,9 @@ impl Decodable for CallType {
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "ipc", binary)]
pub struct Executed {
/// True if the outer call/create resulted in an exceptional exit.
pub exception: Option<evm::Error>,
/// Gas paid up front for execution of transaction.
pub gas: U256,
@@ -178,6 +182,8 @@ pub enum CallError {
TransactionNotFound,
/// Couldn't find requested block's state in the chain.
StatePruned,
/// Couldn't find an amount of gas that didn't result in an exception.
Exceptional,
/// Error executing.
Execution(ExecutionError),
}
@@ -195,6 +201,7 @@ impl fmt::Display for CallError {
let msg = match *self {
TransactionNotFound => "Transaction couldn't be found in the chain".into(),
StatePruned => "Couldn't find the transaction block's state in the chain".into(),
Exceptional => "An exception happened in the execution".into(),
Execution(ref e) => format!("{}", e),
};

View File

@@ -391,6 +391,14 @@ impl Res {
Res::Call(_) | Res::FailedCall(_) | Res::FailedCreate(_) | Res::None => Default::default(),
}
}
/// Did this call fail?
pub fn succeeded(&self) -> bool {
match *self {
Res::Call(_) | Res::Create(_) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
@@ -561,4 +569,3 @@ impl Decodable for VMTrace {
Ok(res)
}
}

View File

@@ -16,7 +16,7 @@
//! Transaction data structure.
use std::ops::Deref;
use std::ops::{Deref, DerefMut};
use std::cell::*;
use rlp::*;
use util::sha3::Hashable;
@@ -102,6 +102,7 @@ impl HeapSizeOf for Transaction {
impl From<ethjson::state::Transaction> for SignedTransaction {
fn from(t: ethjson::state::Transaction) -> Self {
let to: Option<ethjson::hash::Address> = t.to.into();
let secret = Secret::from_slice(&t.secret.0).expect("Valid secret expected.");
Transaction {
nonce: t.nonce.into(),
gas_price: t.gas_price.into(),
@@ -112,7 +113,7 @@ impl From<ethjson::state::Transaction> for SignedTransaction {
},
value: t.value.into(),
data: t.data.into(),
}.sign(&t.secret.into(), None)
}.sign(&secret, None)
}
}
@@ -239,6 +240,12 @@ impl Deref for SignedTransaction {
}
}
impl DerefMut for SignedTransaction {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.unsigned
}
}
impl Decodable for SignedTransaction {
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
let d = decoder.as_rlp();

View File

@@ -166,7 +166,7 @@ pub mod aes {
/// ECDH functions
#[cfg_attr(feature="dev", allow(similar_names))]
pub mod ecdh {
use secp256k1::{ecdh, key};
use secp256k1::{ecdh, key, Error as SecpError};
use ethkey::{Secret, Public, SECP256K1};
use Error;
@@ -180,13 +180,11 @@ pub mod ecdh {
};
let publ = key::PublicKey::from_slice(context, &pdata)?;
// no way to create SecretKey from raw byte array.
let sec: &key::SecretKey = unsafe { ::std::mem::transmute(secret) };
let shared = ecdh::SharedSecret::new_raw(context, &publ, sec);
let sec = key::SecretKey::from_slice(context, &secret)?;
let shared = ecdh::SharedSecret::new_raw(context, &publ, &sec);
let mut s = Secret::default();
s.copy_from_slice(&shared[0..32]);
Ok(s)
Secret::from_slice(&shared[0..32])
.map_err(|_| Error::Secp(SecpError::InvalidSecretKey))
}
}

View File

@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use keccak::Keccak256;
use super::{KeyPair, Error, Generator};
use super::{KeyPair, Error, Generator, Secret};
/// Simple brainwallet.
pub struct Brain(String);
@@ -34,13 +34,15 @@ impl Generator for Brain {
let mut i = 0;
loop {
secret = secret.keccak256();
match i > 16384 {
false => i += 1,
true => {
let result = KeyPair::from_secret(secret.clone().into());
if result.as_ref().ok().map_or(false, |r| r.address()[0] == 0) {
return result;
if let Ok(secret) = Secret::from_slice(&secret) {
let result = KeyPair::from_secret(secret);
if result.as_ref().ok().map_or(false, |r| r.address()[0] == 0) {
return result;
}
}
},
}

View File

@@ -60,11 +60,14 @@ impl KeyPair {
Ok(keypair)
}
pub fn from_secret_slice(slice: &[u8]) -> Result<KeyPair, Error> {
Self::from_secret(Secret::from_slice(slice)?)
}
pub fn from_keypair(sec: key::SecretKey, publ: key::PublicKey) -> Self {
let context = &SECP256K1;
let serialized = publ.serialize_vec(context, false);
let mut secret = Secret::default();
secret.copy_from_slice(&sec[0..32]);
let secret = Secret::from(sec);
let mut public = Public::default();
public.copy_from_slice(&serialized[1..65]);

View File

@@ -29,6 +29,7 @@ mod keccak;
mod prefix;
mod random;
mod signature;
mod secret;
lazy_static! {
pub static ref SECP256K1: secp256k1::Secp256k1 = secp256k1::Secp256k1::new();
@@ -46,10 +47,10 @@ pub use self::keypair::{KeyPair, public_to_address};
pub use self::prefix::Prefix;
pub use self::random::Random;
pub use self::signature::{sign, verify_public, verify_address, recover, Signature};
pub use self::secret::Secret;
use bigint::hash::{H160, H256, H512};
pub type Address = H160;
pub type Secret = H256;
pub type Message = H256;
pub type Public = H512;

69
ethkey/src/secret.rs Normal file
View File

@@ -0,0 +1,69 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use secp256k1::key;
use bigint::hash::H256;
use {Error};
#[derive(Clone, PartialEq, Eq)]
pub struct Secret {
inner: H256,
}
impl fmt::Debug for Secret {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "Secret: 0x{:x}{:x}..{:x}{:x}", self.inner[0], self.inner[1], self.inner[30], self.inner[31])
}
}
impl Secret {
pub fn from_slice(key: &[u8]) -> Result<Self, Error> {
if key.len() != 32 {
return Err(Error::InvalidSecret);
}
let mut h = H256::default();
h.copy_from_slice(&key[0..32]);
Ok(Secret { inner: h })
}
}
impl FromStr for Secret {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let hash = H256::from_str(s).map_err(|e| Error::Custom(format!("{:?}", e)))?;
Self::from_slice(&hash)
}
}
impl From<key::SecretKey> for Secret {
fn from(key: key::SecretKey) -> Self {
Self::from_slice(&key[0..32])
.expect("`key::SecretKey` is valid (no way to construct invalid one); qed")
}
}
impl Deref for Secret {
type Target = H256;
fn deref(&self) -> &Self::Target {
&self.inner
}
}

View File

@@ -16,7 +16,7 @@
use std::ops::{Deref, DerefMut};
use std::cmp::PartialEq;
use std::{mem, fmt};
use std::fmt;
use std::str::FromStr;
use std::hash::{Hash, Hasher};
use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError};
@@ -169,9 +169,8 @@ impl DerefMut for Signature {
pub fn sign(secret: &Secret, message: &Message) -> Result<Signature, Error> {
let context = &SECP256K1;
// no way to create from raw byte array.
let sec: &SecretKey = unsafe { mem::transmute(secret) };
let s = context.sign_recoverable(&SecpMessage::from_slice(&message[..])?, sec)?;
let sec = SecretKey::from_slice(context, &secret)?;
let s = context.sign_recoverable(&SecpMessage::from_slice(&message[..])?, &sec)?;
let (rec_id, data) = s.serialize_compact(context);
let mut data_arr = [0; 65];

View File

@@ -122,16 +122,14 @@ impl Crypto {
return Err(Error::InvalidPassword);
}
let mut secret = Secret::default();
match self.cipher {
Cipher::Aes128Ctr(ref params) => {
let from = 32 - self.ciphertext.len();
crypto::aes::decrypt(&derived_left_bits, &params.iv, &self.ciphertext, &mut (&mut *secret)[from..])
let mut secret = [0; 32];
crypto::aes::decrypt(&derived_left_bits, &params.iv, &self.ciphertext, &mut secret[from..]);
Ok(Secret::from_slice(&secret)?)
},
}
Ok(secret)
}
}

View File

@@ -47,7 +47,7 @@ impl PresaleWallet {
let len = crypto::aes::decrypt_cbc(&derived_key, &self.iv, &self.ciphertext, &mut key).map_err(|_| Error::InvalidPassword)?;
let unpadded = &key[..len];
let secret = Secret::from(unpadded.keccak256());
let secret = Secret::from_slice(&unpadded.keccak256())?;
if let Ok(kp) = KeyPair::from_secret(secret) {
if kp.address() == self.address {
return Ok(kp)

View File

@@ -133,9 +133,9 @@ fn secret_store_load_pat_files() {
#[test]
fn test_decrypting_files_with_short_ciphertext() {
// 31e9d1e6d844bd3a536800ef8d8be6a9975db509, 30
let kp1 = KeyPair::from_secret("000081c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018".into()).unwrap();
let kp1 = KeyPair::from_secret("000081c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018".parse().unwrap()).unwrap();
// d1e64e5480bfaf733ba7d48712decb8227797a4e , 31
let kp2 = KeyPair::from_secret("00fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35".into()).unwrap();
let kp2 = KeyPair::from_secret("00fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35".parse().unwrap()).unwrap();
let dir = DiskDirectory::at(ciphertext_path());
let store = EthStore::open(Box::new(dir)).unwrap();
let accounts = store.accounts().unwrap();

View File

@@ -189,6 +189,7 @@
"valid-url": "1.0.9",
"validator": "6.2.0",
"web3": "0.17.0-beta",
"whatwg-fetch": "2.0.1"
"whatwg-fetch": "2.0.1",
"zxcvbn": "4.4.1"
}
}

View File

@@ -16,6 +16,19 @@
import { stringify } from 'querystring';
export const isServerRunning = (isTestnet = false) => {
const port = isTestnet ? 28443 : 18443;
return fetch(`https://email-verification.parity.io:${port}/health`, {
mode: 'cors', cache: 'no-store'
})
.then((res) => {
return res.ok;
})
.catch(() => {
return false;
});
};
export const postToServer = (query, isTestnet = false) => {
const port = isTestnet ? 28443 : 18443;
query = stringify(query);

View File

@@ -16,6 +16,19 @@
import { stringify } from 'querystring';
export const isServerRunning = (isTestnet = false) => {
const port = isTestnet ? 8443 : 443;
return fetch(`https://sms-verification.parity.io:${port}/health`, {
mode: 'cors', cache: 'no-store'
})
.then((res) => {
return res.ok;
})
.catch(() => {
return false;
});
};
export const postToServer = (query, isTestnet = false) => {
const port = isTestnet ? 8443 : 443;
query = stringify(query);

View File

@@ -23,12 +23,12 @@ import { eventSignature } from '../../util/signature';
export default class Event {
constructor (abi) {
this._name = abi.name;
this._inputs = EventParam.toEventParams(abi.inputs || []);
this._anonymous = !!abi.anonymous;
const { id, signature } = eventSignature(this._name, this.inputParamTypes());
const { id, name, signature } = eventSignature(abi.name, this.inputParamTypes());
this._id = id;
this._name = name;
this._signature = signature;
}

View File

@@ -22,14 +22,14 @@ import { methodSignature } from '../util/signature';
export default class Func {
constructor (abi) {
this._abi = abi;
this._name = abi.name;
this._constant = !!abi.constant;
this._payable = abi.payable;
this._inputs = Param.toParams(abi.inputs || []);
this._outputs = Param.toParams(abi.outputs || []);
const { id, signature } = methodSignature(this._name, this.inputParamTypes());
const { id, name, signature } = methodSignature(abi.name, this.inputParamTypes());
this._id = id;
this._name = name;
this._signature = signature;
}

View File

@@ -35,6 +35,18 @@ describe('abi/spec/Function', () => {
});
describe('constructor', () => {
it('returns signature correctly if name already contains it', () => {
const func = new Func({
name: 'test(bool,string)',
inputs: inputsArr,
outputs: outputsArr
});
expect(func.name).to.equal('test');
expect(func.id).to.equal('test(bool,string)');
expect(func.signature).to.equal('02356205');
});
it('stores the parameters as received', () => {
expect(func.name).to.equal('test');
expect(func.constant).to.be.false;

View File

@@ -17,15 +17,31 @@
import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase
import { fromParamType } from '../spec/paramType/format';
export function eventSignature (name, params) {
export function eventSignature (eventName, params) {
const { strName, name } = parseName(eventName);
const types = (params || []).map(fromParamType).join(',');
const id = `${name || ''}(${types})`;
const id = `${strName}(${types})`;
return { id, signature: keccak_256(id) };
return { id, name, signature: keccak_256(id) };
}
export function methodSignature (name, params) {
const { id, signature } = eventSignature(name, params);
export function methodSignature (methodName, params) {
const { id, name, signature } = eventSignature(methodName, params);
return { id, signature: signature.substr(0, 8) };
return { id, name, signature: signature.substr(0, 8) };
}
function parseName (name) {
const strName = `${name || ''}`;
const idx = strName.indexOf('(');
if (idx === -1) {
return { strName, name };
}
const trimmedName = strName.slice(0, idx);
return {
strName: trimmedName,
name: trimmedName
};
}

View File

@@ -19,50 +19,93 @@ import { eventSignature, methodSignature } from './signature';
describe('abi/util/signature', () => {
describe('eventSignature', () => {
it('encodes signature baz() correctly', () => {
expect(eventSignature('baz', []))
.to.deep.equal({ id: 'baz()', signature: 'a7916fac4f538170f7cd12c148552e2cba9fcd72329a2dd5b07a6fa906488ddf' });
expect(eventSignature('baz', [])).to.deep.equal({
id: 'baz()',
name: 'baz',
signature: 'a7916fac4f538170f7cd12c148552e2cba9fcd72329a2dd5b07a6fa906488ddf'
});
});
it('encodes signature baz(uint32) correctly', () => {
expect(eventSignature('baz', [{ type: 'uint', length: 32 }]))
.to.deep.equal({ id: 'baz(uint32)', signature: '7d68785e8fc871be024b75964bd86d093511d4bc2dc7cf7bea32c48a0efaecb1' });
expect(eventSignature('baz', [{ type: 'uint', length: 32 }])).to.deep.equal({
id: 'baz(uint32)',
name: 'baz',
signature: '7d68785e8fc871be024b75964bd86d093511d4bc2dc7cf7bea32c48a0efaecb1'
});
});
it('encodes signature baz(uint32, bool) correctly', () => {
expect(eventSignature('baz', [{ type: 'uint', length: 32 }, { type: 'bool' }]))
.to.deep.equal({ id: 'baz(uint32,bool)', signature: 'cdcd77c0992ec5bbfc459984220f8c45084cc24d9b6efed1fae540db8de801d2' });
expect(eventSignature('baz', [{ type: 'uint', length: 32 }, { type: 'bool' }])).to.deep.equal({
id: 'baz(uint32,bool)',
name: 'baz',
signature: 'cdcd77c0992ec5bbfc459984220f8c45084cc24d9b6efed1fae540db8de801d2'
});
});
it('encodes no-name signature correctly as ()', () => {
expect(eventSignature(undefined, []))
.to.deep.equal({ id: '()', signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe' });
expect(eventSignature(undefined, [])).to.deep.equal({
id: '()',
name: undefined,
signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe'
});
});
it('encodes no-params signature correctly as ()', () => {
expect(eventSignature(undefined, undefined))
.to.deep.equal({ id: '()', signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe' });
expect(eventSignature(undefined, undefined)).to.deep.equal({
id: '()',
name: undefined,
signature: '861731d50c3880a2ca1994d5ec287b94b2f4bd832a67d3e41c08177bdd5674fe'
});
});
});
describe('methodSignature', () => {
it('encodes signature baz() correctly', () => {
expect(methodSignature('baz', [])).to.deep.equal({ id: 'baz()', signature: 'a7916fac' });
expect(methodSignature('baz', [])).to.deep.equal({
id: 'baz()',
name: 'baz',
signature: 'a7916fac'
});
});
it('encodes signature baz(uint32) correctly', () => {
expect(methodSignature('baz', [{ type: 'uint', length: 32 }])).to.deep.equal({ id: 'baz(uint32)', signature: '7d68785e' });
expect(methodSignature('baz', [{ type: 'uint', length: 32 }])).to.deep.equal({
id: 'baz(uint32)',
name: 'baz',
signature: '7d68785e'
});
});
it('encodes signature baz(uint32, bool) correctly', () => {
expect(methodSignature('baz', [{ type: 'uint', length: 32 }, { type: 'bool' }])).to.deep.equal({ id: 'baz(uint32,bool)', signature: 'cdcd77c0' });
expect(methodSignature('baz', [{ type: 'uint', length: 32 }, { type: 'bool' }])).to.deep.equal({
id: 'baz(uint32,bool)',
name: 'baz',
signature: 'cdcd77c0'
});
});
it('encodes signature in name correctly', () => {
expect(methodSignature('baz(uint32,bool)', [{ type: 'uint', length: 32 }, { type: 'bool' }])).to.deep.equal({
id: 'baz(uint32,bool)',
name: 'baz',
signature: 'cdcd77c0'
});
});
it('encodes no-name signature correctly as ()', () => {
expect(methodSignature(undefined, [])).to.deep.equal({ id: '()', signature: '861731d5' });
expect(methodSignature(undefined, [])).to.deep.equal({
id: '()',
name: undefined,
signature: '861731d5'
});
});
it('encodes no-params signature correctly as ()', () => {
expect(methodSignature(undefined, undefined)).to.deep.equal({ id: '()', signature: '861731d5' });
expect(methodSignature(undefined, undefined)).to.deep.equal({
id: '()',
name: undefined,
signature: '861731d5'
});
});
});
});

View File

@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import EventEmitter from 'eventemitter3';
import { Http, Ws } from './transport';
import Contract from './contract';
@@ -22,8 +24,10 @@ import Subscriptions from './subscriptions';
import util from './util';
import { isFunction } from './util/types';
export default class Api {
export default class Api extends EventEmitter {
constructor (transport) {
super();
if (!transport || !isFunction(transport.execute)) {
throw new Error('EthApi needs transport with execute() function defined');
}

View File

@@ -248,18 +248,32 @@ export default class Contract {
.call(callParams)
.then((encoded) => func.decodeOutput(encoded))
.then((tokens) => tokens.map((token) => token.value))
.then((returns) => returns.length === 1 ? returns[0] : returns);
.then((returns) => returns.length === 1 ? returns[0] : returns)
.catch((error) => {
console.warn(`${func.name}.call`, values, error);
throw error;
});
};
if (!func.constant) {
func.postTransaction = (options, values = []) => {
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.parity.postTransaction(_options);
return this._api.parity
.postTransaction(_options)
.catch((error) => {
console.warn(`${func.name}.postTransaction`, values, error);
throw error;
});
};
func.estimateGas = (options, values = []) => {
const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth.estimateGas(_options);
return this._api.eth
.estimateGas(_options)
.catch((error) => {
console.warn(`${func.name}.estimateGas`, values, error);
throw error;
});
};
}
@@ -385,6 +399,10 @@ export default class Contract {
this._subscribeToChanges();
return subscriptionId;
});
})
.catch((error) => {
console.warn('subscribe', event, _options, error);
throw error;
});
}

View File

@@ -25,6 +25,7 @@ export const ERROR_CODES = {
UNKNOWN_ERROR: -32009,
TRANSACTION_ERROR: -32010,
EXECUTION_ERROR: -32015,
EXCEPTION_ERROR: -32016,
ACCOUNT_LOCKED: -32020,
PASSWORD_INVALID: -32021,
ACCOUNT_ERROR: -32023,

View File

@@ -51,14 +51,14 @@ export default class Http extends JsonRpcBase {
return fetch(this._url, request)
.catch((error) => {
this._connected = false;
this._setDisconnected();
throw error;
})
.then((response) => {
this._connected = true;
this._setConnected();
if (response.status !== 200) {
this._connected = false;
this._setDisconnected();
this.error(JSON.stringify({ status: response.status, statusText: response.statusText }));
console.error(`${method}(${JSON.stringify(params)}): ${response.status}: ${response.statusText}`);

View File

@@ -37,6 +37,26 @@ describe('api/transport/Http', () => {
});
});
describe('transport emitter', () => {
it('emits close event', (done) => {
transport.once('close', () => {
done();
});
transport.execute('eth_call');
});
it('emits open event', (done) => {
mockHttp([{ method: 'eth_call', reply: { result: '' } }]);
transport.once('open', () => {
done();
});
transport.execute('eth_call');
});
});
describe('transport', () => {
const RESULT = ['this is some result'];

View File

@@ -14,8 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default class JsonRpcBase {
import EventEmitter from 'eventemitter3';
export default class JsonRpcBase extends EventEmitter {
constructor () {
super();
this._id = 1;
this._debug = false;
this._connected = false;
@@ -32,6 +36,20 @@ export default class JsonRpcBase {
return json;
}
_setConnected () {
if (!this._connected) {
this._connected = true;
this.emit('open');
}
}
_setDisconnected () {
if (this._connected) {
this._connected = false;
this.emit('close');
}
}
get id () {
return this._id;
}

View File

@@ -22,7 +22,7 @@ import TransportError from '../error';
/* global WebSocket */
export default class Ws extends JsonRpcBase {
constructor (url, token, connect = true) {
constructor (url, token, autoconnect = true) {
super();
this._url = url;
@@ -32,14 +32,14 @@ export default class Ws extends JsonRpcBase {
this._connecting = false;
this._connected = false;
this._lastError = null;
this._autoConnect = false;
this._autoConnect = autoconnect;
this._retries = 0;
this._reconnectTimeoutId = null;
this._connectPromise = null;
this._connectPromiseFunctions = {};
if (connect) {
if (autoconnect) {
this.connect();
}
}
@@ -124,11 +124,8 @@ export default class Ws extends JsonRpcBase {
}
_onOpen = (event) => {
console.log('ws:onOpen');
this._connected = true;
this._setConnected();
this._connecting = false;
this._autoConnect = true;
this._retries = 0;
Object.keys(this._messages)
@@ -142,7 +139,7 @@ export default class Ws extends JsonRpcBase {
}
_onClose = (event) => {
this._connected = false;
this._setDisconnected();
this._connecting = false;
event.timestamp = Date.now();
@@ -209,8 +206,8 @@ export default class Ws extends JsonRpcBase {
if (result.error) {
this.error(event.data);
// Don't print error if request rejected...
if (!/rejected/.test(result.error.message)) {
// Don't print error if request rejected or not is not yet up...
if (!/(rejected|not yet up)/.test(result.error.message)) {
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
}

View File

@@ -21,6 +21,37 @@ describe('api/transport/Ws', () => {
let transport;
let scope;
describe('transport emitter', () => {
const connect = () => {
const scope = mockWs();
const transport = new Ws(TEST_WS_URL);
return { transport, scope };
};
it('emits open event', (done) => {
const { transport, scope } = connect();
transport.once('open', () => {
done();
});
scope.stop();
});
it('emits close event', (done) => {
const { transport, scope } = connect();
transport.once('open', () => {
scope.server.close();
});
transport.once('close', () => {
done();
});
});
});
describe('transport', () => {
let result;

View File

@@ -17,9 +17,12 @@
import { keccak_256 } from 'js-sha3'; // eslint-disable-line
import { hexToBytes } from './format';
import { isHex } from './types';
export function sha3 (value, options) {
if (options && options.encoding === 'hex') {
const forceHex = options && options.encoding === 'hex';
if (forceHex || (!options && isHex(value))) {
const bytes = hexToBytes(value);
return sha3(bytes);
}
@@ -28,3 +31,5 @@ export function sha3 (value, options) {
return `0x${hash}`;
}
sha3.text = (val) => sha3(val, { encoding: 'raw' });

View File

@@ -32,5 +32,14 @@ describe('api/util/sha3', () => {
expect(sha3('01020304', { encoding: 'hex' })).to.equal('0xa6885b3731702da62e8e4a8f584ac46a7f6822f4e2ba50fba902f67b1588d23b');
expect(sha3(Uint8Array.from([1, 2, 3, 4]))).to.equal('0xa6885b3731702da62e8e4a8f584ac46a7f6822f4e2ba50fba902f67b1588d23b');
});
it('should interpret as bytes by default', () => {
expect(sha3('0x01020304')).to.equal('0xa6885b3731702da62e8e4a8f584ac46a7f6822f4e2ba50fba902f67b1588d23b');
});
it('should force text if option is passed', () => {
expect(sha3('0x01020304', { encoding: 'raw' })).to.equal('0x16bff43de576d28857dcba65a56fc17c5e93c09bd6a709268eff8e62025ae869');
expect(sha3.text('0x01020304')).to.equal('0x16bff43de576d28857dcba65a56fc17c5e93c09bd6a709268eff8e62025ae869');
});
});
});

View File

@@ -29,6 +29,10 @@ export function isFunction (test) {
}
export function isHex (_test) {
if (!isString(_test)) {
return false;
}
if (_test.substr(0, 2) === '0x') {
return isHex(_test.slice(2));
}

View File

@@ -66,6 +66,12 @@ describe('api/util/types', () => {
it('correctly identifies non-hex values', () => {
expect(isHex('123j')).to.be.false;
});
it('correctly indentifies non-string values', () => {
expect(isHex(false)).to.be.false;
expect(isHex()).to.be.false;
expect(isHex([1, 2, 3])).to.be.false;
});
});
describe('isInstanceOf', () => {

View File

@@ -17,12 +17,20 @@
import LogLevel from 'loglevel';
export const LOG_KEYS = {
Balances: {
key: 'balances',
desc: 'Balances fetching'
},
TransferModalStore: {
path: 'modals/Transfer/store',
desc: 'Transfer Modal MobX Store'
key: 'modalsTransferStore',
desc: 'Transfer modal MobX store'
},
Signer: {
key: 'secureApi',
desc: 'The Signer and the Secure API'
}
};
export const getLogger = (LOG_KEY) => {
return LogLevel.getLogger(LOG_KEY.path);
return LogLevel.getLogger(LOG_KEY.key);
};

View File

@@ -73,6 +73,26 @@ export default class BadgeReg {
});
}
fetchCertifierByName (name) {
return this
.getContract()
.then((badgeReg) => {
return badgeReg.instance.fromName.call({}, [ name ]);
})
.then(([ id, address, owner ]) => {
if (address === ZERO20) {
throw new Error(`Certifier ${name} does not exist.`);
}
return this.fetchMeta(id)
.then(({ title, icon }) => {
const data = { address, id, name, title, icon };
this.certifiers[id] = data;
return data;
});
});
}
fetchMeta (id) {
return this
.getContract()

View File

@@ -91,7 +91,7 @@ export default class Registry {
lookupAddress (_name) {
const name = _name.toLowerCase();
const sha3 = this._api.util.sha3(name);
const sha3 = this._api.util.sha3.text(name);
return this.getInstance().then((instance) => {
return instance.getAddress.call({}, [sha3, 'A']);

View File

@@ -39,7 +39,7 @@ export const lookup = (name, key) => (dispatch, getState) => {
name = name.toLowerCase();
dispatch(lookupStart(name, key));
getAddress.call({}, [ sha3(name), key ])
getAddress.call({}, [ sha3.text(name), key ])
.then((address) => dispatch(success('lookup', address)))
.catch((err) => {
console.error(`could not lookup ${key} for ${name}`);

View File

@@ -62,7 +62,7 @@ export const reserve = (name) => (dispatch, getState) => {
value: fee
};
const values = [
sha3(name)
sha3.text(name)
];
return postTx(api, reserve, options, values);
@@ -116,7 +116,7 @@ export const drop = (name) => (dispatch, getState) => {
};
const values = [
sha3(name)
sha3.text(name)
];
return postTx(api, drop, options, values);

View File

@@ -54,7 +54,7 @@ export const update = (name, key, value) => (dispatch, getState) => {
};
const values = [
sha3(name),
sha3.text(name),
key,
value
];

View File

@@ -17,7 +17,7 @@
export const getOwner = (contract, name) => {
const { address, api } = contract;
const key = api.util.sha3(name) + '0000000000000000000000000000000000000000000000000000000000000001';
const key = api.util.sha3.text(name) + '0000000000000000000000000000000000000000000000000000000000000001';
const position = api.util.sha3(key, { encoding: 'hex' });
return api

221
js/src/dapps/static/console.css Executable file
View File

@@ -0,0 +1,221 @@
#full-screen {
}
textarea:focus, input:focus{
outline: none;
}
* { padding: 0; margin: 0; }
html, body, #full-screen {
height: 100%;
}
body {
margin: 0;
}
#full-screen {
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow: hidden;
}
#history-wrap {
overflow: auto;
}
#history {
flex: 1 1 auto;
display: flex;
min-height: min-content;
flex-direction: column;
justify-content: flex-end;
}
.entry, #input {
margin: 0;
padding: 0.25em;
display: flex;
flex-direction: row;
align-items: flex-start;
width: 100%;
}
.type {
color: #ccc;
font-weight: bold;
font-family: "Lucida Console", Monaco, monospace;
font-size: 11pt;
padding: 0 0.5em 0 0.25em;
}
.command .type {
color: #aaa;
}
.result .type {
color: #ccc;
}
#input .type {
color: #59f;
}
.addwatch {
background-color: #cfc;
border-top: 1px solid #6e6;
}
.addwatch .type {
color: #040;
}
.addwatch .text {
color: #080;
}
.log.logLevel .type {
color: blue;
}
.log.debugLevel .text {
color: blue;
}
.log.infoLevel .type {
color: #00c;
}
.log.warnLevel {
background-color: #fff8dd;
border-top: 1px solid #fe8;
border-bottom: 1px solid #fe8;
}
.log.warnLevel .text {
color: #440;
}
.log.warnLevel .type {
color: #880;
}
.log.errorLevel, .error {
background-color: #fee;
border-top: 1px solid #fbb;
border-bottom: 1px solid #fbb;
}
.log.errorLevel .text, .error .text {
color: #c00;
}
.log.errorLevel .type, .error .type {
color: #800;
}
span.text {
color: black;
}
.text {
border: 0;
margin: 0;
padding: 0;
color: black;
font-weight: 1000;
font-family: "Lucida Console", Monaco, monospace;
font-size: 11pt;
flex-grow: 1;
}
div.error {
background-color: #fee;
border-top: 1px solid #fbb;
color: red;
}
div.command {
border-top: 1px solid #eee;
}
div#input {
border-top: 1px solid #eee;
}
#status {
background: #eee;
padding: 0.25em;
font-family: "Lucida Console", Monaco, monospace;
font-size: 8pt;
color: #888;
flex: 0 0 auto;
}
#input {
flex: 0 0 auto;
}
#status {
display: flex;
}
.watch {
padding-left: 1em;
padding-right: 1em;
}
.watch:not(:first-child) {
border-left: 1px solid #ccc;
}
.expr {
color: #888;
margin-right: 0.5em;
}
.res {
font-weight: bold;
color: black;
}
.stringType {
color: #c00;
}
.numberType {
color: #00f;
}
.eObject {
color: #00f;
}
.objectType {
font-style: italic;
}
.fieldType {
color: #808;
}
.undefinedType {
color: #888;
}
.functionType {
color: #666;
font-style: italic;
}
#autocomplete-anchor {
position: relative;
}
#autocomplete {
position: absolute;
left: 1.5em;
bottom: 0;
background: #f8f8f8;
box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.5);
max-height: 20em;
overflow: scroll;
}
#autocomplete div {
font-size: small;
font-family: "Lucida Console", Monaco, monospace;
padding: 0.1em 1em 0.1em 0;
border-top: 1px solid #eee;
}
.ac-already {
font-weight: bold;
}
.ac-new {
color: #666;
}
.ac-selected {
background-color: #ccddff;
}

View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="us">
<head>
<meta charset="utf-8">
<title>JS Console dapp</title>
<link href="console.css" rel='stylesheet' type='text/css'>
<script src="/parity-utils/parity.js"></script>
<script src="/parity-utils/web3.js"></script>
</head>
<body>
<div id="full-screen">
<div id="eval"></div>
<div id="history-wrap"><div id="history"></div></div>
<div id="autocomplete-anchor"><div id="autocomplete"></div></div>
<div id="input"><span class="type">&gt;</span><input type="text" class="text" id="command" list="input-datalist"></div>
<div id="status"></div>
</div>
<script src="console.js"></script>
</body>
</html>

602
js/src/dapps/static/console.js Executable file
View File

@@ -0,0 +1,602 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable */
// TODO: Fix linting issues
if (typeof(window.parity) == 'object')
window.api = window.parent.secureApi;
window.parity.api.subscribe('eth_blockNumber', function (error, blockNumber) {
if (error) {
console.log('error', error);
return;
}
refreshWatches();
});
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function getAllPropertyNames(obj) {
var props = {};
do {
Object.getOwnPropertyNames(obj).forEach(n => props[n] = true);
} while (obj = Object.getPrototypeOf(obj));
return Object.keys(props);
}
function htmlToElement(html) {
var template = document.createElement('template');
template.innerHTML = html;
return template.content.firstChild;
}
function evaluate(x) {
try {
return eval(x);
}
catch (err) {
return eval('(()=>{var x = ' + x + "; return x;})()")
}
}
function displayReady(x) {
if (x === undefined)
return '<span class="undefinedType">undefined</span>';
if (x === null)
return '<span class="undefinedType">null</span>';
if (typeof(x) == "string")
return `"<span class="${typeof(x)}Type">${escapeHtml(x)}</span>"`;
if (Object.prototype.toString.call(x) === '[object Array]')
return `[${x.map(displayReady).join(', ')}]`;
if (typeof(x) == "function")
return `<span class="${typeof(x)}Type">function () { /* ... */ }</span>`;
if (typeof(x) == "object") {
if (x.toString().indexOf('[object ') != 0)
return `<span class="${x.constructor.name}Object">${escapeHtml(x.toString())}</span>`;
return `<span class="objectType ${x.constructor.name}Object">${x.constructor.name} {${Object.keys(x).map(f => `<span class="fieldType">${escapeHtml(f)}</span>: ${displayReady(x[f])}`).join(', ')}}</span>`;
}
return `<span class="${typeof(x)}Type">${escapeHtml(JSON.stringify(x))}</span>`;
}
if (!localStorage.history)
localStorage.history = "[]";
window.historyData = JSON.parse(localStorage.history);
window.historyIndex = window.historyData.length;
if (!localStorage.watches)
localStorage.watches = "[]";
window.watches = {};
function watch(name, f) {
let status = document.getElementById("status");
let cleanName = name.replace(/[^a-zA-Z0-9]/, '');
status.innerHTML += `<div class="watch" id="watch_${cleanName}"><span class="expr" id="expr_${cleanName}">${escapeHtml(name)}</span><span class="res" id="res_${cleanName}"></span></div>`;
window.watches[name] = f;
}
var savedWatches = JSON.parse(localStorage.watches);
savedWatches.forEach(w => watch(w[1], () => evaluate(w[0])));
if (typeof(window.web3) == 'object' && window.watches.latest == undefined)
watch('latest', () => window.web3.eth.blockNumber);
function refreshWatches() {
for (n in window.watches) {
let r = window.watches[n]();
let cn = n.replace(/[^a-zA-Z0-9]/, '');
let e = document.getElementById(`res_${cn}`);
if (typeof(r) == 'object' && r.constructor.name == "Promise")
r.then(r => e.innerHTML = displayReady(r));
else
e.innerHTML = displayReady(r);
}
}
function removeWatch(name) {
let e = document.getElementById(`watch_${name}`);
e.parentNode.removeChild(e);
delete window.watches[name];
}
function newLog(level, text) {
let icon = {
debug: "&nbsp;",
log: "&nbsp;",
warn: "⚠",
error: "✖",
info: ""
};
pushLine('<div class="entry log ' + level + 'Level"><span class="type">' + icon[level] + '</span><span class="text">' + escapeHtml(text) + '</span></div>');
}
function exec() {
let command = document.getElementById("command");
let c = command.value;
if (c != '') {
command.value = "";
window.historyData.push(c);
while (window.historyData.length > 1000)
window.historyData.shift;
localStorage.history = JSON.stringify(window.historyData);
window.historyIndex = window.historyData.length;
var html = '';
if (c.indexOf("//") == 0) {
let n = c.substr(2);
savedWatches = savedWatches.filter(x => x[1] != n);
localStorage.watches = JSON.stringify(savedWatches);
removeWatch(n);
}
else if (c.indexOf("//") != -1) {
x = c.split("//");
let e = x[0];
savedWatches.push(x);
localStorage.watches = JSON.stringify(savedWatches);
watch(x[1], () => evaluate(e));
pushLine('<div class="entry command"><span class="type">&gt;</span><span class="text">' + escapeHtml(c) + '</span></div>');
pushLine('<div class="entry addwatch"><span class="type">✓</span><span class="text">Watch added</span></div>');
}
else {
pushLine('<div class="entry command"><span class="type">&gt;</span><span class="text">' + escapeHtml(c) + '</span></div>');
let res;
try {
res = evaluate(c);
if (typeof(res) == 'object' && res !== null && res.constructor.name == "Promise") {
let id = window.historyData.length;
pushLine('<div class="entry result"><span class="type">&lt;</span><span class="text" id="pending' + id + '">...</span></div>');
res.then(r => document.getElementById('pending' + id).innerHTML = displayReady(r));
} else {
pushLine('<div class="entry result"><span class="type">&lt;</span><span class="text">' + displayReady(res) + '</span></div>');
}
}
catch (err) {
pushLine('<div class="entry error"><span class="type">✖</span><span class="text">Unhandled exception: ' + escapeHtml(err.message) + '</span></div>');
}
}
}
refreshWatches();
}
function pushLine(l) {
document.getElementById("history").innerHTML += l
var h = document.getElementById("history-wrap");
h.scrollTop = h.scrollHeight;
}
var autocompletes = [];
var currentAuto = null;
var currentPots = [];
var currentStem = null;
function updateAutocomplete() {
let v = document.getElementById("command").value;
if (v.length == 0) {
cancelAutocomplete();
return;
}
let t = v.split('.');
let last = t.pop();
let tj = t.join('.');
let ex = t.length > 0 ? tj : 'window';
if (currentStem != tj) {
autocompletes = eval('getAllPropertyNames('+ex+')');
currentStem = tj;
}
let dl = document.getElementById("autocomplete");
currentPots = autocompletes.filter(n => n.startsWith(last));
if (currentPots.length > 0) {
if (currentPots.indexOf(currentAuto) == -1)
currentAuto = currentPots[0];
dl.innerHTML = currentPots
// .map(n => `${tj != '' ? tj + '.' : ''}${n}`)
.map((n, i) => `<div id="pot${i}" class="${currentAuto == n ? 'ac-selected' : 'ac-unselected'}"><span class="ac-already">${escapeHtml(last)}</span><span class="ac-new">${escapeHtml(n.substr(last.length))}</div>`)
.join('');
dl.hidden = false;
} else {
cancelAutocomplete();
}
}
function enactAutocomplete() {
if (currentAuto != null) {
document.getElementById("command").value = (currentStem != '' ? currentStem + '.' : '') + currentAuto;
cancelAutocomplete();
}
}
function cancelAutocomplete() {
document.getElementById("autocomplete").hidden = true;
currentAuto = null;
}
function scrollAutocomplete(positive) {
if (currentAuto != null) {
var i = currentPots.indexOf(currentAuto);
document.getElementById('pot' + i).classList = ['ac-unselected'];
if (positive && i < currentPots.length - 1)
++i;
else if (!positive && i > 0)
--i;
currentAuto = currentPots[i];
let sel = document.getElementById('pot' + i);
sel.classList = ['ac-selected'];
sel.scrollIntoViewIfNeeded();
}
}
document.getElementById("command").addEventListener("paste", updateAutocomplete);
document.getElementById("command").addEventListener("input", updateAutocomplete);
document.getElementById("command").addEventListener("focusout", cancelAutocomplete);
document.getElementById("command").addEventListener("blur", cancelAutocomplete);
document.getElementById("command").addEventListener("keydown", function(event) {
let el = document.getElementById("command");
if (currentAuto != null) {
if (event.keyCode == 38 || event.keyCode == 40) {
event.preventDefault();
scrollAutocomplete(event.keyCode == 40);
}
else if ((event.keyCode == 39 || event.keyCode == 9 || event.keyCode == 13) && el.selectionStart == el.value.length) {
event.preventDefault();
enactAutocomplete();
}
else if (event.keyCode == 27) {
event.preventDefault();
cancelAutocomplete();
}
} else {
let command = document.getElementById("command");
if (event.keyCode == 38 && window.historyIndex > 0) {
event.preventDefault();
window.historyIndex--;
command.value = window.historyData[window.historyIndex];
}
if (event.keyCode == 40 && window.historyIndex < window.historyData.length) {
event.preventDefault();
window.historyIndex++;
command.value = window.historyIndex < window.historyData.length ? window.historyData[window.historyIndex] : "";
}
}
if (event.keyCode >= 48 || event.keyCode == 8) {
let t = document.getElementById("command").value;
setTimeout(() => {
if (t != document.getElementById("command").value)
updateAutocomplete();
}, 0);
}
else {
setTimeout(() => {
if (el.selectionStart != el.value.length)
cancelAutocomplete();
}, 0);
}
});
document.getElementById("command").addEventListener("keyup", function(event) {
if (event.keyCode == 13) {
event.preventDefault();
exec();
}
});
if (typeof(window.parity) == 'object') {
document.getElementById("command").focus();
window.web3 = web3;
window.parity = parity;
}
refreshWatches();
["debug", "error", "info", "log", "warn"].forEach(n => { let old = window.console[n]; window.console[n] = x => { old(x); newLog(n, x); }; });
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///// Home comforts.
// Usage example:
// web3.eth.traceCall({
// to: theChicken.address,
// data: theChicken.withdraw.getData(100000000000000000),
// gas: 100000
// },
// `["trace", "vmTrace", "stateDiff"]
// )
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'traceCall',
call: 'trace_call',
params: 2,
inputFormatter: [web3._extend.formatters.inputCallFormatter, null]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'traceSendRawTransaction',
call: 'trace_rawTransaction',
params: 2,
inputFormatter: [null, null]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'traceReplayTransaction',
call: 'trace_replayTransaction',
params: 2,
inputFormatter: [null, null]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'setMode',
call: 'ethcore_setMode',
params: 1,
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'mode',
call: 'ethcore_mode',
params: 0,
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'traceTransaction',
call: 'trace_Transaction',
params: 1,
inputFormatter: [null]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'gasPriceStatistics',
call: 'ethcore_gasPriceStatistics',
params: 0,
outputFormatter: function(a) { return a.map(web3.toBigNumber); }
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'registryAddress',
call: 'ethcore_registryAddress',
params: 0
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'accountsInfo',
call: 'personal_accountsInfo',
outputFormatter: function(m) { Object.keys(m).forEach(k => {
m[k].meta = JSON.parse(m[k].meta);
m[k].meta.name = m[k].name;
m[k].meta.uuid = m[k].uuid;
m[k] = m[k].meta;
}); return m; },
params: 0
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'setAccountName',
call: 'personal_setAccountName',
params: 2,
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'setAccountMeta',
call: 'personal_setAccountMeta',
params: 2,
inputFormatter: [a => a, JSON.stringify]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'postTransaction',
call: 'eth_postTransaction',
params: 1,
inputFormatter: [web3._extend.formatters.inputCallFormatter]
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'postSign',
call: 'eth_postSign',
params: 1
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'encryptMessage',
call: 'ethcore_encryptMessage',
params: 2
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'checkRequest',
call: 'eth_checkRequest',
params: 1
})
]
});
web3._extend({
property: 'eth',
methods: [
new web3._extend.Method({
name: 'listAccounts',
call: 'ethcore_listAccounts',
params: 0
})
]
});
{
var postTransaction = web3.eth.postTransaction.bind(web3.eth);
var sendTransaction = web3.eth.sendTransaction.bind(web3.eth);
web3.eth.sendTransaction = function(options, f) {
// No callback - do sync API.
if (typeof f != "function")
return sendTransaction(options);
// Callback - use async API.
var id = postTransaction(options);
console.log("Posted trasaction id=" + id);
var timerId = window.setInterval(check, 500);
function check() {
try {
let r = web3.eth.checkRequest(id);
if (typeof r == 'string') {
clearInterval(timerId);
if (r == "0x0000000000000000000000000000000000000000000000000000000000000000")
f("Rejected", r);
else
f(null, r);
} else if (r !== null) {
console.log("checkRequest returned: " + r);
}
}
catch (e) {
clearInterval(timerId);
f("Rejected", null);
}
}
}
}
web3.eth.installInterceptor = function(interceptor) {
var oldSendTransaction = web3.eth.sendTransaction.bind(web3.eth);
web3.eth.sendTransaction = function(options, f) {
if (interceptor(options) == false)
return "0x0000000000000000000000000000000000000000000000000000000000000000";
return oldSendTransaction(options, f);
};
}
web3.eth.reporter = function(e, r) {
if (e) {
console.log("Error confirming transaction: " + e);
} else {
var addr = r;
var confirmed = false;
var timer_id = window.setInterval(check, 500);
function check() {
var receipt = web3.eth.getTransactionReceipt(addr);
if (receipt != null) {
if (!confirmed) {
console.log("Transaction confirmed (" + r + "); used " + receipt.gasUsed + " gas; left " + receipt.logs.length + " logs; mining...");
confirmed = true;
}
if (typeof receipt.blockHash == 'string') {
clearInterval(timer_id);
console.log("Mined into block " + receipt.blockNumber);
}
}
}
}
}
{
var oldSha3 = web3.sha3
web3.sha3 = function(data, format) {
if (typeof format !== 'string' || (format != 'hex' && format != 'bin'))
format = data.startsWith('0x') ? 'hex' : 'bin';
return oldSha3(data, {encoding: format});
}
}
{
var Registry = web3.eth.contract([{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"}],"name":"confirmReverse","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserve","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"bytes32"}],"name":"set","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"drop","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"}],"name":"setFee","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_to","type":"address"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserved","outputs":[{"name":"reserved","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_who","type":"address"}],"name":"proposeReverse","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"reverse","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"uint256"}],"name":"setUint","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"removeReverse","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_key","type":"string"},{"name":"_value","type":"address"}],"name":"setAddress","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Drained","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"FeeChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"owner","type":"address"}],"name":"Reserved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"oldOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"Transferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"owner","type":"address"}],"name":"Dropped","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"key","type":"string"},{"indexed":false,"name":"plainKey","type":"string"}],"name":"DataChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":true,"name":"reverse","type":"address"}],"name":"ReverseProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":true,"name":"reverse","type":"address"}],"name":"ReverseConfirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":true,"name":"reverse","type":"address"}],"name":"ReverseRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]);
web3.eth.registry = Registry.at(web3.eth.registryAddress());
web3.eth.registry.lookup = (name, field) => web3.eth.registry.get(web3.sha3(name), field);
web3.eth.registry.lookupAddress = (name, field) => web3.eth.registry.getAddress(web3.sha3(name), field);
web3.eth.registry.lookupUint = (name, field) => web3.eth.registry.getUint(web3.sha3(name), field);
var TokenReg = web3.eth.contract([{"constant":true,"inputs":[{"name":"_id","type":"uint256"}],"name":"token","outputs":[{"name":"addr","type":"address"},{"name":"tla","type":"string"},{"name":"base","type":"uint256"},{"name":"name","type":"string"},{"name":"owner","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"},{"name":"_tla","type":"string"},{"name":"_base","type":"uint256"},{"name":"_name","type":"string"}],"name":"register","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_fee","type":"uint256"}],"name":"setFee","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"},{"name":"_key","type":"bytes32"}],"name":"meta","outputs":[{"name":"","type":"bytes32"}],"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"},{"name":"_tla","type":"string"},{"name":"_base","type":"uint256"},{"name":"_name","type":"string"},{"name":"_owner","type":"address"}],"name":"registerAs","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_tla","type":"string"}],"name":"fromTLA","outputs":[{"name":"id","type":"uint256"},{"name":"addr","type":"address"},{"name":"base","type":"uint256"},{"name":"name","type":"string"},{"name":"owner","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"tokenCount","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"}],"name":"unregister","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"fromAddress","outputs":[{"name":"id","type":"uint256"},{"name":"tla","type":"string"},{"name":"base","type":"uint256"},{"name":"name","type":"string"},{"name":"owner","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_key","type":"bytes32"},{"name":"_value","type":"bytes32"}],"name":"setMeta","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"tla","type":"string"},{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"addr","type":"address"},{"indexed":false,"name":"name","type":"string"}],"name":"Registered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"tla","type":"string"},{"indexed":true,"name":"id","type":"uint256"}],"name":"Unregistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":true,"name":"key","type":"bytes32"},{"indexed":false,"name":"value","type":"bytes32"}],"name":"MetaChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]);
web3.eth.tokenReg = TokenReg.at(web3.eth.registry.lookupAddress('tokenreg', 'A'));
}
/* eslint-enable */

View File

@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import IconButton from 'material-ui/IconButton';
import { IconButton } from 'material-ui';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';

View File

@@ -88,12 +88,13 @@ export default class RecoveryPhrase extends Component {
value={ password2 }
onChange={ this.onEditPassword2 } />
</div>
<Checkbox
className={ styles.checkbox }
label='Key was created with Parity <1.4.5 on Windows'
checked={ windowsPhrase }
onCheck={ this.onToggleWindowsPhrase } />
</div>
<Checkbox
className={ styles.checkbox }
label='Key was created with Parity <1.4.5 on Windows'
checked={ windowsPhrase }
onCheck={ this.onToggleWindowsPhrase }
/>
</Form>
);
}

View File

@@ -16,6 +16,7 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ActionDone from 'material-ui/svg-icons/action/done';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
@@ -100,44 +101,45 @@ export default class CreateAccount extends Component {
switch (stage) {
case 0:
return (
<CreationType
onChange={ this.onChangeType } />
<CreationType onChange={ this.onChangeType } />
);
case 1:
if (createType === 'fromNew') {
return (
<NewAccount
onChange={ this.onChangeDetails } />
<NewAccount onChange={ this.onChangeDetails } />
);
} else if (createType === 'fromGeth') {
}
if (createType === 'fromGeth') {
return (
<NewGeth
accounts={ accounts }
onChange={ this.onChangeGeth } />
onChange={ this.onChangeGeth }
/>
);
} else if (createType === 'fromPhrase') {
}
if (createType === 'fromPhrase') {
return (
<RecoveryPhrase
onChange={ this.onChangeDetails } />
<RecoveryPhrase onChange={ this.onChangeDetails } />
);
} else if (createType === 'fromRaw') {
}
if (createType === 'fromRaw') {
return (
<RawKey
onChange={ this.onChangeDetails } />
<RawKey onChange={ this.onChangeDetails } />
);
}
return (
<NewImport
onChange={ this.onChangeWallet } />
<NewImport onChange={ this.onChangeWallet } />
);
case 2:
if (createType === 'fromGeth') {
return (
<AccountDetailsGeth
addresses={ this.state.gethAddresses } />
<AccountDetailsGeth addresses={ this.state.gethAddresses } />
);
}
@@ -145,7 +147,8 @@ export default class CreateAccount extends Component {
<AccountDetails
address={ this.state.address }
name={ this.state.name }
phrase={ this.state.phrase } />
phrase={ this.state.phrase }
/>
);
}
}
@@ -210,11 +213,14 @@ export default class CreateAccount extends Component {
}
return (
<Warning warning={
<FormattedMessage
id='createAccount.warning.insecurePassword'
defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.' />
} />
<Warning
warning={
<FormattedMessage
id='createAccount.warning.insecurePassword'
defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.'
/>
}
/>
);
}
@@ -371,7 +377,7 @@ export default class CreateAccount extends Component {
}
onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => {
this.setState({
const nextState = {
canCreate,
name,
passwordHint,
@@ -380,7 +386,9 @@ export default class CreateAccount extends Component {
phrase,
windowsPhrase: windowsPhrase || false,
rawKey
});
};
this.setState(nextState);
}
onChangeRaw = (canCreate, rawKey) => {

View File

@@ -391,6 +391,10 @@ class DeployContract extends Component {
.then(([gasEst, gas]) => {
this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasStore.setGas(gas.toFixed(0));
})
.catch((error) => {
this.gasStore.setEstimatedError();
console.warn('estimateGas', error);
});
}

View File

@@ -20,7 +20,6 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { toWei } from '~/api/util/wei';
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash, Warning } from '~/ui';
@@ -156,8 +155,7 @@ class ExecuteContract extends Component {
}
return (
<Warning
warning={ errorEstimated } />
<Warning warning={ errorEstimated } />
);
}
@@ -379,6 +377,7 @@ class ExecuteContract extends Component {
this.gasStore.setGas(gas.toFixed(0));
})
.catch((error) => {
this.gasStore.setEstimatedError();
console.warn('estimateGas', error);
});
}
@@ -470,11 +469,7 @@ function mapStateToProps (initState, initProps) {
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(ExecuteContract);

View File

@@ -68,13 +68,13 @@ const STORE = {
};
function createApi (result = true) {
const sha3 = sinon.stub().resolves('0x0000000000000000000000000000000000000000');
sha3.text = sha3;
return {
parity: {
registryAddress: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
},
util: {
sha3: sinon.stub().resolves('0x0000000000000000000000000000000000000000')
}
util: { sha3 }
};
}

View File

@@ -23,7 +23,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError, openSnackbar } from '~/redux/actions';
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
import { Button, Modal, IdentityName, IdentityIcon, PasswordStrength } from '~/ui';
import Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
@@ -120,7 +120,7 @@ class PasswordManager extends Component {
}
renderPage () {
const { busy, isRepeatValid, passwordHint } = this.store;
const { busy, isRepeatValid, newPassword, passwordHint } = this.store;
return (
<Tabs
@@ -176,8 +176,6 @@ class PasswordManager extends Component {
defaultMessage='current password' />
}
onChange={ this.onEditCurrentPassword }
onSubmit={ this.changePassword }
submitOnBlur={ false }
type='password' />
<Input
disabled={ busy }
@@ -192,8 +190,6 @@ class PasswordManager extends Component {
defaultMessage='(optional) new password hint' />
}
onChange={ this.onEditNewPasswordHint }
onSubmit={ this.changePassword }
submitOnBlur={ false }
value={ passwordHint } />
<div className={ styles.passwords }>
<div className={ styles.password }>
@@ -240,6 +236,8 @@ class PasswordManager extends Component {
type='password' />
</div>
</div>
<PasswordStrength input={ newPassword } />
</div>
</Form>
</Tab>

View File

@@ -23,7 +23,7 @@ import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract';
import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store';
import { getLogger, LOG_KEYS } from '~/config';
@@ -220,13 +220,27 @@ export default class TransferStore {
}
@action _attachWalletOperation = (txhash) => {
if (!txhash || /^(0x)?0*$/.test(txhash)) {
return;
}
let ethSubscriptionId = null;
// Number of blocks left to look-up (unsub after 15 blocks if nothing)
let nBlocksLeft = 15;
return this.api.subscribe('eth_blockNumber', () => {
this.api.eth
.getTransactionReceipt(txhash)
.then((tx) => {
if (nBlocksLeft <= 0) {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
return;
}
if (!tx) {
nBlocksLeft--;
return;
}
@@ -239,6 +253,10 @@ export default class TransferStore {
this.operation = operations[0];
}
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
})
.catch(() => {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
});
@@ -357,6 +375,7 @@ export default class TransferStore {
});
})
.catch((error) => {
this.gasStore.setEstimatedError();
console.warn('etimateGas', error);
this.recalculate(redo);
});
@@ -441,6 +460,8 @@ export default class TransferStore {
const gasTotal = new BigNumber(_gasTotal || 0);
const { valueAll, isEth, isWallet } = this;
log.debug('@getValues', 'gas', gasTotal.toFormat());
if (!valueAll) {
const value = this.getTokenValue();
@@ -568,6 +589,7 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
log.debug('@send', 'transfer value', options.value && options.value.toFormat());
return this._getTransferMethod().postTransaction(options, values);
}
@@ -626,7 +648,8 @@ export default class TransferStore {
options.gas = MAX_GAS_ESTIMATION;
}
const { token } = this.getValues(options.gas);
const gasTotal = new BigNumber(options.gas || DEFAULT_GAS).mul(options.gasPrice || DEFAULT_GASPRICE);
const { token } = this.getValues(gasTotal);
if (isEth && !isWallet && !forceToken) {
options.value = token;

View File

@@ -27,7 +27,7 @@ const STEP_UPDATING = 1;
const STEP_COMPLETED = 2;
const STEP_ERROR = 2;
const CHECK_INTERVAL = 1 * A_MINUTE;
let instance = null;
export default class Store {
@observable available = null;
@@ -44,8 +44,6 @@ export default class Store {
this.loadStorage();
this.checkUpgrade();
setInterval(this.checkUpgrade, CHECK_INTERVAL);
}
@computed get isVisible () {
@@ -119,10 +117,10 @@ export default class Store {
checkUpgrade = () => {
if (!this._api) {
return;
return Promise.resolve(false);
}
Promise
return Promise
.all([
this._api.parity.upgradeReady(),
this._api.parity.consensusCapability(),
@@ -134,11 +132,23 @@ export default class Store {
}
this.setVersions(available, version, consensusCapability);
return true;
})
.catch((error) => {
console.warn('checkUpgrade', error);
return false;
});
}
static get (api) {
if (!instance) {
instance = new Store(api);
}
return instance;
}
}
export {

View File

@@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import BigNumber from 'bignumber.js';
import { Checkbox } from 'material-ui';
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
@@ -33,10 +34,11 @@ import styles from './gatherData.css';
export default class GatherData extends Component {
static propTypes = {
fee: React.PropTypes.instanceOf(BigNumber),
method: PropTypes.string.isRequired,
fields: PropTypes.array.isRequired,
isVerified: nullableProptype(PropTypes.bool.isRequired),
hasRequested: nullableProptype(PropTypes.bool.isRequired),
isServerRunning: nullableProptype(PropTypes.bool.isRequired),
isVerified: nullableProptype(PropTypes.bool.isRequired),
method: PropTypes.string.isRequired,
setConsentGiven: PropTypes.func.isRequired
}
@@ -48,13 +50,19 @@ export default class GatherData extends Component {
return (
<Form>
{ howItWorks }
{ this.renderServerRunning() }
{ this.renderFee() }
{ this.renderCertified() }
{ this.renderRequested() }
{ this.renderFields() }
<Checkbox
className={ styles.spacing }
label={ 'I agree to the terms and conditions below.' }
label={
<FormattedMessage
id='ui.verification.gatherData.termsOfService'
defaultMessage='I agree to the terms and conditions below.'
/>
}
disabled={ isVerified }
onCheck={ this.consentOnChange }
/>
@@ -63,16 +71,75 @@ export default class GatherData extends Component {
);
}
renderServerRunning () {
const { isServerRunning } = this.props;
if (isServerRunning) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isServerRunning.true'
defaultMessage='The verification server is running.'
/>
</p>
</div>
);
} else if (isServerRunning === false) {
return (
<div className={ styles.container }>
<ErrorIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isServerRunning.false'
defaultMessage='The verification server is not running.'
/>
</p>
</div>
);
}
return (
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isServerRunning.pending'
defaultMessage='Checking if the verification server is running…'
/>
</p>
);
}
renderFee () {
const { fee } = this.props;
if (!fee) {
return (<p>Fetching the fee</p>);
}
if (fee.eq(0)) {
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.nofee'
defaultMessage='There is no additional fee.'
/>
</p>
</div>
);
}
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>The fee is { fromWei(fee).toFixed(3) } ETH.</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.fee'
defaultMessage='The additional fee is {amount} ETH.'
values={ {
amount: fromWei(fee).toFixed(3)
} }
/>
</p>
</div>
);
}
@@ -84,19 +151,34 @@ export default class GatherData extends Component {
return (
<div className={ styles.container }>
<ErrorIcon />
<p className={ styles.message }>Your account is already verified.</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.true'
defaultMessage='Your account is already verified.'
/>
</p>
</div>
);
} else if (isVerified === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>Your account is not verified yet.</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.false'
defaultMessage='Your account is not verified yet.'
/>
</p>
</div>
);
}
return (
<p className={ styles.message }>Checking if your account is verified</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.isVerified.pending'
defaultMessage='Checking if your account is verified…'
/>
</p>
);
}
@@ -112,19 +194,34 @@ export default class GatherData extends Component {
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>You already requested verification.</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.true'
defaultMessage='You already requested verification.'
/>
</p>
</div>
);
} else if (hasRequested === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>You did not request verification yet.</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.false'
defaultMessage='You did not request verification yet.'
/>
</p>
</div>
);
}
return (
<p className={ styles.message }>Checking if you requested verification</p>
<p className={ styles.message }>
<FormattedMessage
id='ui.verification.gatherData.hasRequested.pending'
defaultMessage='Checking if you requested verification…'
/>
</p>
);
}

View File

@@ -21,9 +21,10 @@ import EmailVerificationABI from '~/contracts/abi/email-verification.json';
import VerificationStore, {
LOADING, QUERY_DATA, QUERY_CODE, POSTED_CONFIRMATION, DONE
} from './store';
import { postToServer } from '../../3rdparty/email-verification';
import { isServerRunning, postToServer } from '../../3rdparty/email-verification';
const EMAIL_VERIFICATION = 7; // id in the `BadgeReg.sol` contract
// name in the `BadgeReg.sol` contract
const EMAIL_VERIFICATION = 'emailverification';
export default class EmailVerificationStore extends VerificationStore {
@observable email = '';
@@ -59,7 +60,11 @@ export default class EmailVerificationStore extends VerificationStore {
super(api, EmailVerificationABI, EMAIL_VERIFICATION, account, isTestnet);
}
requestValues = () => [ sha3(this.email) ]
isServerRunning = () => {
return isServerRunning(this.isTestnet);
}
requestValues = () => [ sha3.text(this.email) ]
@action setEmail = (email) => {
this.email = email;

View File

@@ -21,9 +21,10 @@ import SMSVerificationABI from '~/contracts/abi/sms-verification.json';
import VerificationStore, {
LOADING, QUERY_DATA, QUERY_CODE, POSTED_CONFIRMATION, DONE
} from './store';
import { postToServer } from '../../3rdparty/sms-verification';
import { isServerRunning, postToServer } from '../../3rdparty/sms-verification';
const SMS_VERIFICATION = 0; // id in the `BadgeReg.sol` contract
// name in the `BadgeReg.sol` contract
const SMS_VERIFICATION = 'smsverification';
export default class SMSVerificationStore extends VerificationStore {
@observable number = '';
@@ -58,6 +59,10 @@ export default class SMSVerificationStore extends VerificationStore {
super(api, SMSVerificationABI, SMS_VERIFICATION, account, isTestnet);
}
isServerRunning = () => {
return isServerRunning(this.isTestnet);
}
@action setNumber = (number) => {
this.number = number;
}

View File

@@ -40,19 +40,20 @@ export default class VerificationStore {
@observable fee = null;
@observable isVerified = null;
@observable hasRequested = null;
@observable isServerRunning = null;
@observable consentGiven = false;
@observable requestTx = null;
@observable code = '';
@observable isCodeValid = null;
@observable confirmationTx = null;
constructor (api, abi, certifierId, account, isTestnet) {
constructor (api, abi, certifierName, account, isTestnet) {
this.api = api;
this.account = account;
this.isTestnet = isTestnet;
this.step = LOADING;
Contracts.get().badgeReg.fetchCertifier(certifierId)
Contracts.get().badgeReg.fetchCertifierByName(certifierName)
.then(({ address }) => {
this.contract = new Contract(api, abi).at(address);
this.load();
@@ -73,6 +74,14 @@ export default class VerificationStore {
const { contract, account } = this;
this.step = LOADING;
const isServerRunning = this.isServerRunning()
.then((isRunning) => {
this.isServerRunning = isRunning;
})
.catch((err) => {
this.error = 'Failed to check if server is running: ' + err.message;
});
const fee = contract.instance.fee.call()
.then((fee) => {
this.fee = fee;
@@ -101,7 +110,7 @@ export default class VerificationStore {
});
Promise
.all([ fee, isVerified, hasRequested ])
.all([ isServerRunning, fee, isVerified, hasRequested ])
.then(() => {
this.step = QUERY_DATA;
});
@@ -120,7 +129,7 @@ export default class VerificationStore {
const confirm = contract.functions.find((fn) => fn.name === 'confirm');
const options = { from: account };
const values = [ sha3(code) ];
const values = [ sha3.text(code) ];
this.code = code;
this.isCodeValid = null;
@@ -192,7 +201,7 @@ export default class VerificationStore {
@action sendConfirmation = () => {
const { api, account, contract, code } = this;
const token = sha3(code);
const token = sha3.text(code);
const confirm = contract.functions.find((fn) => fn.name === 'confirm');
const options = { from: account };

View File

@@ -94,10 +94,10 @@ class Verification extends Component {
return (
<Modal
actions={ this.renderDialogActions(phase, error, isStepValid) }
title='verify your account'
visible
current={ phase }
steps={ ['Method', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
title='verify your account'
visible
waiting={ error ? [] : [ 2, 4 ] }
>
{ this.renderStep(phase, error) }
@@ -111,8 +111,9 @@ class Verification extends Component {
const cancel = (
<Button
key='cancel' label='Cancel'
icon={ <CancelIcon /> }
key='cancel'
label='Cancel'
onClick={ onClose }
/>
);
@@ -125,9 +126,10 @@ class Verification extends Component {
<div>
{ cancel }
<Button
key='done' label='Done'
disabled={ !isStepValid }
icon={ <DoneIcon /> }
key='done'
label='Done'
onClick={ onClose }
/>
</div>
@@ -160,9 +162,15 @@ class Verification extends Component {
<div>
{ cancel }
<Button
key='next' label='Next'
disabled={ !isStepValid }
icon={ <IdentityIcon address={ account } button /> }
icon={
<IdentityIcon
address={ account }
button
/>
}
key='next'
label='Next'
onClick={ action }
/>
</div>
@@ -180,16 +188,16 @@ class Verification extends Component {
const value = values.findIndex((v) => v.value === method);
return (
<RadioButtons
onChange={ this.selectMethod }
value={ value < 0 ? 0 : value }
values={ values }
onChange={ this.selectMethod }
/>
);
}
const {
step,
fee, isVerified, hasRequested,
isServerRunning, fee, isVerified, hasRequested,
requestTx, isCodeValid, confirmationTx,
setCode
} = this.store;
@@ -223,15 +231,21 @@ class Verification extends Component {
return (
<GatherData
fee={ fee }
hasRequested={ hasRequested }
isServerRunning={ isServerRunning }
isVerified={ isVerified }
method={ method } fields={ fields }
fee={ fee } isVerified={ isVerified } hasRequested={ hasRequested }
setConsentGiven={ setConsentGiven }
/>
);
case 2:
return (
<SendRequest step={ step } tx={ requestTx } />
<SendRequest
step={ step }
tx={ requestTx }
/>
);
case 3:
@@ -245,16 +259,19 @@ class Verification extends Component {
}
return (
<QueryCode
receiver={ receiver }
hint={ hint }
isCodeValid={ isCodeValid }
receiver={ receiver }
setCode={ setCode }
/>
);
case 4:
return (
<SendConfirmation step={ step } tx={ confirmationTx } />
<SendConfirmation
step={ step }
tx={ confirmationTx }
/>
);
case 5:

View File

@@ -21,51 +21,164 @@ import { padRight } from '~/api/util/format';
import Contracts from '~/contracts';
let instance = null;
export default class Balances {
constructor (store, api) {
this._api = api;
this._store = store;
this._tokenregSubId = null;
this._tokenregMetaSubId = null;
this._tokenreg = null;
this._tokenregSID = null;
this._tokenMetaSID = null;
// Throttled `retrieveTokens` function
this._blockNumberSID = null;
this._accountsInfoSID = null;
// Throtthled load tokens (no more than once
// every minute)
this.loadTokens = throttle(
this._loadTokens,
60 * 1000,
{ leading: true, trailing: true }
);
// Throttled `_fetchBalances` function
// that gets called max once every 40s
this.longThrottledFetch = throttle(
this.fetchBalances,
this._fetchBalances,
40 * 1000,
{ trailing: true }
{ leading: false, trailing: true }
);
this.shortThrottledFetch = throttle(
this.fetchBalances,
this._fetchBalances,
2 * 1000,
{ trailing: true }
{ leading: false, trailing: true }
);
// Fetch all tokens every 2 minutes
this.throttledTokensFetch = throttle(
this.fetchTokens,
60 * 1000,
{ trailing: true }
this._fetchTokens,
2 * 60 * 1000,
{ leading: false, trailing: true }
);
// Unsubscribe previous instance if it exists
if (instance) {
Balances.stop();
}
}
start () {
this.subscribeBlockNumber();
this.subscribeAccountsInfo();
static get (store = {}) {
if (!instance && store) {
const { api } = store.getState();
return Balances.instantiate(store, api);
}
this.loadTokens();
return instance;
}
static instantiate (store, api) {
if (!instance) {
instance = new Balances(store, api);
}
return instance;
}
static start () {
if (!instance) {
return Promise.reject('BalancesProvider has not been intiated yet');
}
const self = instance;
// Unsubscribe from previous subscriptions
return Balances
.stop()
.then(() => self.loadTokens())
.then(() => {
const promises = [
self.subscribeBlockNumber(),
self.subscribeAccountsInfo()
];
return Promise.all(promises);
});
}
static stop () {
if (!instance) {
return Promise.resolve();
}
const self = instance;
const promises = [];
if (self._blockNumberSID) {
const p = self._api
.unsubscribe(self._blockNumberSID)
.then(() => {
self._blockNumberSID = null;
});
promises.push(p);
}
if (self._accountsInfoSID) {
const p = self._api
.unsubscribe(self._accountsInfoSID)
.then(() => {
self._accountsInfoSID = null;
});
promises.push(p);
}
// Unsubscribe without adding the promises
// to the result, since it would have to wait for a
// reconnection to resolve if the Node is disconnected
if (self._tokenreg) {
if (self._tokenregSID) {
const tokenregSID = self._tokenregSID;
self._tokenreg
.unsubscribe(tokenregSID)
.then(() => {
if (self._tokenregSID === tokenregSID) {
self._tokenregSID = null;
}
});
}
if (self._tokenMetaSID) {
const tokenMetaSID = self._tokenMetaSID;
self._tokenreg
.unsubscribe(tokenMetaSID)
.then(() => {
if (self._tokenMetaSID === tokenMetaSID) {
self._tokenMetaSID = null;
}
});
}
}
return Promise.all(promises);
}
subscribeAccountsInfo () {
this._api
return this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (error) {
return;
}
this.fetchBalances();
this.fetchAllBalances();
})
.then((accountsInfoSID) => {
this._accountsInfoSID = accountsInfoSID;
})
.catch((error) => {
console.warn('_subscribeAccountsInfo', error);
@@ -73,49 +186,97 @@ export default class Balances {
}
subscribeBlockNumber () {
this._api
return this._api
.subscribe('eth_blockNumber', (error) => {
if (error) {
return console.warn('_subscribeBlockNumber', error);
}
const { syncing } = this._store.getState().nodeStatus;
this.throttledTokensFetch();
// If syncing, only retrieve balances once every
// few seconds
if (syncing) {
this.shortThrottledFetch.cancel();
return this.longThrottledFetch();
}
this.longThrottledFetch.cancel();
return this.shortThrottledFetch();
return this.fetchAllBalances();
})
.then((blockNumberSID) => {
this._blockNumberSID = blockNumberSID;
})
.catch((error) => {
console.warn('_subscribeBlockNumber', error);
});
}
fetchBalances () {
this._store.dispatch(fetchBalances());
fetchAllBalances (options = {}) {
// If it's a network change, reload the tokens
// ( and then fetch the tokens balances ) and fetch
// the accounts balances
if (options.changedNetwork) {
this.loadTokens({ skipNotifications: true });
this.loadTokens.flush();
this.fetchBalances({
force: true,
skipNotifications: true
});
return;
}
this.fetchTokensBalances(options);
this.fetchBalances(options);
}
fetchTokens () {
this._store.dispatch(fetchTokensBalances());
fetchTokensBalances (options) {
const { skipNotifications = false, force = false } = options;
this.throttledTokensFetch(skipNotifications);
if (force) {
this.throttledTokensFetch.flush();
}
}
fetchBalances (options) {
const { skipNotifications = false, force = false } = options;
const { syncing } = this._store.getState().nodeStatus;
// If syncing, only retrieve balances once every
// few seconds
if (syncing) {
this.shortThrottledFetch.cancel();
this.longThrottledFetch(skipNotifications);
if (force) {
this.longThrottledFetch.flush();
}
return;
}
this.longThrottledFetch.cancel();
this.shortThrottledFetch(skipNotifications);
if (force) {
this.shortThrottledFetch.flush();
}
}
_fetchBalances (skipNotifications = false) {
this._store.dispatch(fetchBalances(null, skipNotifications));
}
_fetchTokens (skipNotifications = false) {
this._store.dispatch(fetchTokensBalances(null, null, skipNotifications));
}
getTokenRegistry () {
return Contracts.get().tokenReg.getContract();
}
loadTokens () {
this
_loadTokens (options = {}) {
return this
.getTokenRegistry()
.then((tokenreg) => {
this._tokenreg = tokenreg;
this._store.dispatch(setTokenReg(tokenreg));
this._store.dispatch(loadTokens());
this._store.dispatch(loadTokens(options));
return this.attachToTokens(tokenreg);
})
@@ -133,7 +294,7 @@ export default class Balances {
}
attachToNewToken (tokenreg) {
if (this._tokenregSubId) {
if (this._tokenregSID) {
return Promise.resolve();
}
@@ -149,13 +310,13 @@ export default class Balances {
this.handleTokensLogs(logs);
})
.then((tokenregSubId) => {
this._tokenregSubId = tokenregSubId;
.then((tokenregSID) => {
this._tokenregSID = tokenregSID;
});
}
attachToTokenMetaChange (tokenreg) {
if (this._tokenregMetaSubId) {
if (this._tokenMetaSID) {
return Promise.resolve();
}
@@ -172,8 +333,8 @@ export default class Balances {
this.handleTokensLogs(logs);
})
.then((tokenregMetaSubId) => {
this._tokenregMetaSubId = tokenregMetaSubId;
.then((tokenMetaSID) => {
this._tokenMetaSID = tokenMetaSID;
});
}

View File

@@ -23,18 +23,27 @@ import { setAddressImage } from './imagesActions';
import * as ABIS from '~/contracts/abi';
import { notifyTransaction } from '~/util/notifications';
import { LOG_KEYS, getLogger } from '~/config';
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
const log = getLogger(LOG_KEYS.Balances);
const ETH = {
name: 'Ethereum',
tag: 'ETH',
image: imagesEthereum
};
function setBalances (_balances) {
function setBalances (_balances, skipNotifications = false) {
return (dispatch, getState) => {
const state = getState();
const currentTokens = Object.values(state.balances.tokens || {});
const currentTags = [ 'eth' ]
.concat(currentTokens.map((token) => token.tag))
.filter((tag) => tag)
.map((tag) => tag.toLowerCase());
const accounts = state.personal.accounts;
const nextBalances = _balances;
const prevBalances = state.balances.balances;
@@ -48,38 +57,55 @@ function setBalances (_balances) {
const balance = Object.assign({}, balances[address]);
const { tokens, txCount = balance.txCount } = nextBalances[address];
const nextTokens = balance.tokens.slice();
tokens.forEach((t) => {
const { token, value } = t;
const { tag } = token;
const prevTokens = balance.tokens.slice();
const nextTokens = [];
const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag);
currentTags
.forEach((tag) => {
const prevToken = prevTokens.find((tok) => tok.token.tag.toLowerCase() === tag);
const nextToken = tokens.find((tok) => tok.token.tag.toLowerCase() === tag);
if (tokenIndex === -1) {
nextTokens.push({
token,
value
});
} else {
const oldValue = nextTokens[tokenIndex].value;
// If the given token is not in the current tokens, skip
if (!nextToken && !prevToken) {
return false;
}
// No updates
if (!nextToken) {
return nextTokens.push(prevToken);
}
const { token, value } = nextToken;
// If it's a new token, push it
if (!prevToken) {
return nextTokens.push({
token, value
});
}
// Otherwise, update the value
const prevValue = prevToken.value;
// If received a token/eth (old value < new value), notify
if (oldValue.lt(value) && accounts[address]) {
if (prevValue.lt(value) && accounts[address] && !skipNotifications) {
const account = accounts[address];
const txValue = value.minus(oldValue);
const txValue = value.minus(prevValue);
const redirectToAccount = () => {
const route = `/account/${account.address}`;
const route = `/accounts/${account.address}`;
dispatch(push(route));
};
notifyTransaction(account, token, txValue, redirectToAccount);
}
nextTokens[tokenIndex] = { token, value };
}
});
return nextTokens.push({
...prevToken,
value
});
});
balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens };
});
@@ -123,7 +149,9 @@ export function setTokenImage (tokenAddress, image) {
};
}
export function loadTokens () {
export function loadTokens (options = {}) {
log.debug('loading tokens', Object.keys(options).length ? options : '');
return (dispatch, getState) => {
const { tokenreg } = getState().balances;
@@ -131,7 +159,7 @@ export function loadTokens () {
.call()
.then((numTokens) => {
const tokenIds = range(numTokens.toNumber());
dispatch(fetchTokens(tokenIds));
dispatch(fetchTokens(tokenIds, options));
})
.catch((error) => {
console.warn('balances::loadTokens', error);
@@ -139,8 +167,9 @@ export function loadTokens () {
};
}
export function fetchTokens (_tokenIds) {
export function fetchTokens (_tokenIds, options = {}) {
const tokenIds = uniq(_tokenIds || []);
return (dispatch, getState) => {
const { api, images, balances } = getState();
const { tokenreg } = balances;
@@ -161,8 +190,9 @@ export function fetchTokens (_tokenIds) {
dispatch(setAddressImage(address, image, true));
});
log.debug('fetched token', tokens);
dispatch(setTokens(tokens));
dispatch(fetchBalances());
dispatch(updateTokensFilter(null, null, options));
})
.catch((error) => {
console.warn('balances::fetchTokens', error);
@@ -170,7 +200,7 @@ export function fetchTokens (_tokenIds) {
};
}
export function fetchBalances (_addresses) {
export function fetchBalances (_addresses, skipNotifications = false) {
return (dispatch, getState) => {
const { api, personal } = getState();
const { visibleAccounts, accounts } = personal;
@@ -192,8 +222,7 @@ export function fetchBalances (_addresses) {
balances[addr] = accountsBalances[idx];
});
dispatch(setBalances(balances));
updateTokensFilter(addresses)(dispatch, getState);
dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchBalances', error);
@@ -201,7 +230,7 @@ export function fetchBalances (_addresses) {
};
}
export function updateTokensFilter (_addresses, _tokens) {
export function updateTokensFilter (_addresses, _tokens, options = {}) {
return (dispatch, getState) => {
const { api, balances, personal } = getState();
const { visibleAccounts, accounts } = personal;
@@ -214,27 +243,32 @@ export function updateTokensFilter (_addresses, _tokens) {
const tokenAddresses = tokens.map((t) => t.address).sort();
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
// Has the tokens addresses changed (eg. a network change)
const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses);
const sameAddresses = isEqual(addresses, tokensFilter.addresses);
if (sameTokens && sameAddresses) {
// Addresses that are not in the current filter (omit those
// that the filter includes)
const newAddresses = addresses.filter((address) => !tokensFilter.addresses.includes(address));
// If no new addresses and the same tokens, don't change the filter
if (sameTokens && newAddresses.length === 0) {
log.debug('no need to update token filter', addresses, tokenAddresses, tokensFilter);
return queryTokensFilter(tokensFilter)(dispatch, getState);
}
}
let promise = Promise.resolve();
log.debug('updating the token filter', addresses, tokenAddresses);
const promises = [];
if (tokensFilter.filterFromId) {
promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId));
promises.push(api.eth.uninstallFilter(tokensFilter.filterFromId));
}
if (tokensFilter.filterToId) {
promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterToId));
promises.push(api.eth.uninstallFilter(tokensFilter.filterToId));
}
if (tokenAddresses.length === 0 || addresses.length === 0) {
return promise;
}
const promise = Promise.all(promises);
const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
@@ -269,8 +303,10 @@ export function updateTokensFilter (_addresses, _tokens) {
addresses, tokenAddresses
};
const { skipNotifications } = options;
dispatch(setTokensFilter(nextTokensFilter));
fetchTokensBalances(addresses, tokens)(dispatch, getState);
fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState);
})
.catch((error) => {
console.warn('balances::updateTokensFilter', error);
@@ -326,7 +362,7 @@ export function queryTokensFilter (tokensFilter) {
};
}
export function fetchTokensBalances (_addresses = null, _tokens = null) {
export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) {
return (dispatch, getState) => {
const { api, personal, balances } = getState();
const { visibleAccounts, accounts } = personal;
@@ -348,7 +384,7 @@ export function fetchTokensBalances (_addresses = null, _tokens = null) {
balances[addr] = tokensBalances[idx];
});
dispatch(setBalances(balances));
dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchTokensBalances', error);

View File

@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BalancesProvider from './balances';
import { showSnackbar } from './snackbarActions';
import { DEFAULT_NETCHAIN } from './statusReducer';
@@ -29,6 +30,11 @@ export default class ChainMiddleware {
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
// Fetch the new balances without notifying the user of any change
BalancesProvider.get(store).fetchAllBalances({
changedNetwork: true
});
}
}
}

View File

@@ -16,13 +16,18 @@
import sinon from 'sinon';
import Contracts from '~/contracts';
import { initialState as defaultNodeStatusState } from './statusReducer';
import ChainMiddleware from './chainMiddleware';
import { createWsApi } from '~/../test/e2e/ethapi';
let middleware;
let next;
let store;
const api = createWsApi();
Contracts.create(api);
function createMiddleware (collection = {}) {
middleware = new ChainMiddleware().toMiddleware();
next = sinon.stub();
@@ -30,6 +35,7 @@ function createMiddleware (collection = {}) {
dispatch: sinon.stub(),
getState: () => {
return {
api: api,
nodeStatus: Object.assign({}, defaultNodeStatusState, collection)
};
}

View File

@@ -16,7 +16,8 @@
import { isEqual, intersection } from 'lodash';
import { fetchBalances } from './balancesActions';
import BalancesProvider from './balances';
import { updateTokensFilter } from './balancesActions';
import { attachWallets } from './walletActions';
import Contract from '~/api/contract';
@@ -75,6 +76,9 @@ export function personalAccountsInfo (accountsInfo) {
return wallet;
});
})
.catch(() => {
return [];
})
.then((_wallets) => {
_wallets.forEach((wallet) => {
const owners = wallet.owners.map((o) => o.address);
@@ -95,7 +99,14 @@ export function personalAccountsInfo (accountsInfo) {
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
dispatch(fetchBalances());
BalancesProvider.get().fetchAllBalances({
force: true
});
})
.catch((error) => {
console.warn('personalAccountsInfo', error);
throw error;
});
};
}
@@ -123,6 +134,18 @@ export function setVisibleAccounts (addresses) {
}
dispatch(_setVisibleAccounts(addresses));
dispatch(fetchBalances(addresses));
// Don't update the balances if no new addresses displayed
if (addresses.length === 0) {
return;
}
// Update the Tokens filter to take into account the new
// addresses
dispatch(updateTokensFilter());
BalancesProvider.get().fetchBalances({
force: true
});
};
}

View File

@@ -14,33 +14,110 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
import { isEqual } from 'lodash';
import { LOG_KEYS, getLogger } from '~/config';
import UpgradeStore from '~/modals/UpgradeParity/store';
import BalancesProvider from './balances';
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
const log = getLogger(LOG_KEYS.Signer);
let instance = null;
export default class Status {
_apiStatus = {};
_status = {};
_longStatus = {};
_minerSettings = {};
_timeoutIds = {};
_blockNumberSubscriptionId = null;
_timestamp = Date.now();
constructor (store, api) {
this._api = api;
this._store = store;
this._upgradeStore = UpgradeStore.get(api);
this._apiStatus = {};
this._status = {};
this._longStatus = {};
this._minerSettings = {};
// On connecting, stop all subscriptions
api.on('connecting', this.stop, this);
this._longStatusTimeoutId = null;
// On connected, start the subscriptions
api.on('connected', this.start, this);
this._timestamp = Date.now();
// On disconnected, stop all subscriptions
api.on('disconnected', this.stop, this);
this.updateApiStatus();
}
static instantiate (store, api) {
if (!instance) {
instance = new Status(store, api);
}
return instance;
}
start () {
this._subscribeBlockNumber();
this._pollStatus();
this._pollLongStatus();
this._pollLogs();
log.debug('status::start');
Promise
.all([
this._subscribeBlockNumber(),
this._pollLogs(),
this._pollLongStatus(),
this._pollStatus()
])
.then(() => {
return BalancesProvider.start();
});
}
_subscribeBlockNumber () {
this._api
stop () {
log.debug('status::stop');
const promises = [];
if (this._blockNumberSubscriptionId) {
const promise = this._api
.unsubscribe(this._blockNumberSubscriptionId)
.then(() => {
this._blockNumberSubscriptionId = null;
});
promises.push(promise);
}
Object.values(this._timeoutIds).forEach((timeoutId) => {
clearTimeout(timeoutId);
});
const promise = BalancesProvider.stop();
promises.push(promise);
return Promise.all(promises)
.then(() => true)
.catch((error) => {
console.error('status::stop', error);
return true;
})
.then(() => this.updateApiStatus());
}
updateApiStatus () {
const apiStatus = this.getApiStatus();
log.debug('status::updateApiStatus', apiStatus);
if (!isEqual(apiStatus, this._apiStatus)) {
this._store.dispatch(statusCollection(apiStatus));
this._apiStatus = apiStatus;
}
}
_subscribeBlockNumber = () => {
return this._api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
return;
@@ -51,6 +128,10 @@ export default class Status {
this._api.eth
.getBlockByNumber(blockNumber)
.then((block) => {
if (!block) {
return;
}
this._store.dispatch(statusCollection({
blockTimestamp: block.timestamp,
gasLimit: block.gasLimit
@@ -59,6 +140,9 @@ export default class Status {
.catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
});
})
.then((blockNumberSubscriptionId) => {
this._blockNumberSubscriptionId = blockNumberSubscriptionId;
});
}
@@ -72,11 +156,7 @@ export default class Status {
.catch(() => false);
}
_pollStatus = () => {
const nextTimeout = (timeout = 1000) => {
setTimeout(() => this._pollStatus(), timeout);
};
getApiStatus = () => {
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
const apiStatus = {
@@ -86,19 +166,23 @@ export default class Status {
secureToken
};
const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected;
return apiStatus;
}
if (gotConnected) {
this._pollLongStatus();
}
_pollStatus = () => {
const nextTimeout = (timeout = 1000) => {
if (this._timeoutIds.status) {
clearTimeout(this._timeoutIds.status);
}
if (!isEqual(apiStatus, this._apiStatus)) {
this._store.dispatch(statusCollection(apiStatus));
this._apiStatus = apiStatus;
}
this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout);
};
if (!isConnected) {
return nextTimeout(250);
this.updateApiStatus();
if (!this._api.isConnected) {
nextTimeout(250);
return Promise.resolve();
}
const { refreshStatus } = this._store.getState().nodeStatus;
@@ -110,7 +194,7 @@ export default class Status {
statusPromises.push(this._api.eth.hashrate());
}
Promise
return Promise
.all(statusPromises)
.then(([ syncing, ...statusResults ]) => {
const status = statusResults.length === 0
@@ -125,11 +209,11 @@ export default class Status {
this._store.dispatch(statusCollection(status));
this._status = status;
}
nextTimeout();
})
.catch((error) => {
console.error('_pollStatus', error);
})
.then(() => {
nextTimeout();
});
}
@@ -140,7 +224,7 @@ export default class Status {
* from the UI
*/
_pollMinerSettings = () => {
Promise
return Promise
.all([
this._api.eth.coinbase(),
this._api.parity.extraData(),
@@ -175,21 +259,21 @@ export default class Status {
*/
_pollLongStatus = () => {
if (!this._api.isConnected) {
return;
return Promise.resolve();
}
const nextTimeout = (timeout = 30000) => {
if (this._longStatusTimeoutId) {
clearTimeout(this._longStatusTimeoutId);
if (this._timeoutIds.longStatus) {
clearTimeout(this._timeoutIds.longStatus);
}
this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout);
this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(), timeout);
};
// Poll Miner settings just in case
this._pollMinerSettings();
const minerPromise = this._pollMinerSettings();
Promise
const mainPromise = Promise
.all([
this._api.parity.netPeers(),
this._api.web3.clientVersion(),
@@ -198,10 +282,11 @@ export default class Status {
this._api.parity.netChain(),
this._api.parity.netPort(),
this._api.parity.rpcSettings(),
this._api.parity.enode()
this._api.parity.enode(),
this._upgradeStore.checkUpgrade()
])
.then(([
netPeers, clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
netPeers, clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode, upgradeStatus
]) => {
const isTest =
netVersion === '2' || // morden
@@ -225,21 +310,31 @@ export default class Status {
})
.catch((error) => {
console.error('_pollLongStatus', error);
})
.then(() => {
nextTimeout(60000);
});
nextTimeout(60000);
return Promise.all([ minerPromise, mainPromise ]);
}
_pollLogs = () => {
const nextTimeout = (timeout = 1000) => setTimeout(this._pollLogs, timeout);
const nextTimeout = (timeout = 1000) => {
if (this._timeoutIds.logs) {
clearTimeout(this._timeoutIds.logs);
}
this._timeoutIds.logs = setTimeout(this._pollLogs, timeout);
};
const { devLogsEnabled } = this._store.getState().nodeStatus;
if (!devLogsEnabled) {
nextTimeout();
return;
return Promise.resolve();
}
Promise
return Promise
.all([
this._api.parity.devLogs(),
this._api.parity.devLogsLevels()
@@ -249,11 +344,12 @@ export default class Status {
devLogs: devLogs.slice(-1024),
devLogsLevels
}));
nextTimeout();
})
.catch((error) => {
console.error('_pollLogs', error);
nextTimeout();
})
.then(() => {
return nextTimeout();
});
}
}

View File

@@ -38,10 +38,10 @@ export default function (api, browserHistory) {
const middleware = initMiddleware(api, browserHistory);
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
new BalancesProvider(store, api).start();
BalancesProvider.instantiate(store, api);
StatusProvider.instantiate(store, api);
new PersonalProvider(store, api).start();
new SignerProvider(store, api).start();
new StatusProvider(store, api).start();
store.dispatch(loadWallet(api));
setupWorker(store);

View File

@@ -17,133 +17,34 @@
import { uniq } from 'lodash';
import Api from './api';
import { LOG_KEYS, getLogger } from '~/config';
const log = getLogger(LOG_KEYS.Signer);
const sysuiToken = window.localStorage.getItem('sysuiToken');
export default class SecureApi extends Api {
_isConnecting = false;
_needsToken = false;
_tokens = [];
_dappsInterface = null;
_dappsPort = 8080;
_signerPort = 8180;
constructor (url, nextToken) {
super(new Api.Transport.Ws(url, sysuiToken, false));
const transport = new Api.Transport.Ws(url, sysuiToken, false);
super(transport);
this._url = url;
this._isConnecting = true;
this._needsToken = false;
this._dappsPort = 8080;
this._dappsInterface = null;
this._signerPort = 8180;
// Try tokens from localstorage, then from hash
// Try tokens from localstorage, from hash and 'initial'
this._tokens = uniq([sysuiToken, nextToken, 'initial'])
.filter((token) => token)
.map((token) => ({ value: token, tried: false }));
this._tryNextToken();
}
saveToken = () => {
window.localStorage.setItem('sysuiToken', this._transport.token);
// DEBUG: console.log('SecureApi:saveToken', this._transport.token);
}
/**
* Returns a Promise that gets resolved with
* a boolean: `true` if the node is up, `false`
* otherwise
*/
_checkNodeUp () {
const url = this._url.replace(/wss?/, 'http');
return fetch(url, { method: 'HEAD' })
.then(
(r) => r.status === 200,
() => false
)
.catch(() => false);
}
_setManual () {
this._needsToken = true;
this._isConnecting = false;
}
_tryNextToken () {
const nextTokenIndex = this._tokens.findIndex((t) => !t.tried);
if (nextTokenIndex < 0) {
return this._setManual();
}
const nextToken = this._tokens[nextTokenIndex];
nextToken.tried = true;
this.updateToken(nextToken.value);
}
_followConnection = () => {
const token = this.transport.token;
return this
.transport
.connect()
.then(() => {
if (token === 'initial') {
return this.signer
.generateAuthorizationToken()
.then((token) => {
return this.updateToken(token);
})
.catch((e) => console.error(e));
}
this.connectSuccess();
return true;
})
.catch((e) => {
this
._checkNodeUp()
.then((isNodeUp) => {
// Try again in a few...
if (!isNodeUp) {
this._isConnecting = false;
const timeout = this.transport.retryTimeout;
window.setTimeout(() => {
this._followConnection();
}, timeout);
return;
}
this._tryNextToken();
return false;
});
});
}
connectSuccess () {
this._isConnecting = false;
this._needsToken = false;
this.saveToken();
Promise
.all([
this.parity.dappsPort(),
this.parity.dappsInterface(),
this.parity.signerPort()
])
.then(([dappsPort, dappsInterface, signerPort]) => {
this._dappsPort = dappsPort.toNumber();
this._dappsInterface = dappsInterface;
this._signerPort = signerPort.toNumber();
});
// DEBUG: console.log('SecureApi:connectSuccess', this._transport.token);
}
updateToken (token) {
this._transport.updateToken(token.replace(/[^a-zA-Z0-9]/g, ''), false);
return this._followConnection();
// DEBUG: console.log('SecureApi:updateToken', this._transport.token, connectState);
// When the transport is closed, try to reconnect
transport.on('close', this.connect, this);
this.connect();
}
get dappsPort () {
@@ -151,17 +52,19 @@ export default class SecureApi extends Api {
}
get dappsUrl () {
let hostname;
return `http://${this.hostname}:${this.dappsPort}`;
}
get hostname () {
if (window.location.hostname === 'home.parity') {
hostname = 'dapps.parity';
} else if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
hostname = window.location.hostname;
} else {
hostname = this._dappsInterface;
return 'dapps.parity';
}
return `http://${hostname}:${this._dappsPort}`;
if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
return window.location.hostname;
}
return this._dappsInterface;
}
get signerPort () {
@@ -183,4 +86,279 @@ export default class SecureApi extends Api {
get secureToken () {
return this._transport.token;
}
connect () {
if (this._isConnecting) {
return;
}
log.debug('trying to connect...');
this._isConnecting = true;
this.emit('connecting');
// Reset the tested Tokens
this._resetTokens();
// Try to connect
return this._connect()
.then((connected) => {
this._isConnecting = false;
if (connected) {
const token = this.secureToken;
log.debug('got connected ; saving token', token);
// Save the sucessful token
this._saveToken(token);
this._needsToken = false;
// Emit the connected event
return this.emit('connected');
}
// If not connected, we need a new token
log.debug('needs a token');
this._needsToken = true;
return this.emit('disconnected');
})
.catch((error) => {
this._isConnecting = false;
log.debug('emitting "disconnected"');
this.emit('disconnected');
console.error('unhandled error in secureApi', error);
});
}
/**
* Returns a Promise that gets resolved with
* a boolean: `true` if the node is up, `false`
* otherwise (HEAD request to the Node)
*/
isNodeUp () {
const url = this._url.replace(/wss?/, 'http');
return fetch(url, { method: 'HEAD' })
.then(
(r) => r.status === 200,
() => false
)
.catch(() => false);
}
/**
* Update the given token, ie. add it to the token
* list, and then try to connect (if not already connecting)
*/
updateToken (_token) {
const token = this._sanitiseToken(_token);
log.debug('updating token', token);
// Update the tokens list: put the new one on first position
this._tokens = [ { value: token, tried: false } ].concat(this._tokens);
// Try to connect with the new token added
return this.connect();
}
/**
* Try to connect to the Node with the next Token in
* the list
*/
_connect () {
log.debug('trying next token');
// Get the first not-tried token
const nextToken = this._getNextToken();
// If no more tokens to try, user has to enter a new one
if (!nextToken) {
return Promise.resolve(false);
}
nextToken.tried = true;
return this._connectWithToken(nextToken.value)
.then((validToken) => {
// If not valid, try again with the next token in the list
if (!validToken) {
return this._connect();
}
// If correct and valid token, wait until the Node is ready
// and resolve as connected
return this._waitUntilNodeReady()
.then(() => this._fetchSettings())
.then(() => true);
})
.catch((error) => {
log.error('unkown error in _connect', error);
return false;
});
}
/**
* Connect with the given token.
* It returns a Promise that gets resolved
* with `validToken` as argument, whether the given token
* is valid or not
*/
_connectWithToken (_token) {
// Sanitize the token first
const token = this._sanitiseToken(_token);
// Update the token in the transport layer
this.transport.updateToken(token, false);
log.debug('connecting with token', token);
return this.transport.connect()
.then(() => {
log.debug('connected with', token);
if (token === 'initial') {
return this._generateAuthorizationToken();
}
// The token is valid !
return true;
})
.catch((error) => {
// Log if it's not a close error (ie. wrong token)
if (error && error.type !== 'close') {
log.debug('did not connect ; error', error);
}
// Check if the Node is up
return this.isNodeUp()
.then((isNodeUp) => {
// If it's not up, try again in a few...
if (!isNodeUp) {
const timeout = this.transport.retryTimeout;
log.debug('node is not up ; will try again in', timeout, 'ms');
return new Promise((resolve, reject) => {
window.setTimeout(() => {
this._connectWithToken(token).then(resolve).catch(reject);
}, timeout);
});
}
// The token is invalid
log.debug('tried with a wrong token', token);
return false;
});
});
}
/**
* Retrieve the correct ports from the Node
*/
_fetchSettings () {
return Promise
.all([
this.parity.dappsPort(),
this.parity.dappsInterface(),
this.parity.signerPort()
])
.then(([dappsPort, dappsInterface, signerPort]) => {
this._dappsPort = dappsPort.toNumber();
this._dappsInterface = dappsInterface;
this._signerPort = signerPort.toNumber();
});
}
/**
* Try to generate an Authorization Token.
* Then try to connect with the new token.
*/
_generateAuthorizationToken () {
return this.signer
.generateAuthorizationToken()
.then((token) => this._connectWithToken(token));
}
/**
* Get the next token to try, if any left
*/
_getNextToken () {
// Get the first not-tried token
const nextTokenIndex = this._tokens.findIndex((t) => !t.tried);
// If no more tokens to try, user has to enter a new one
if (nextTokenIndex < 0) {
return null;
}
const nextToken = this._tokens[nextTokenIndex];
return nextToken;
}
_resetTokens () {
this._tokens = this._tokens.map((token) => ({
...token,
tried: false
}));
}
_sanitiseToken (token) {
return token.replace(/[^a-zA-Z0-9]/g, '');
}
_saveToken (token) {
window.localStorage.setItem('sysuiToken', token);
}
/**
* Promise gets resolved when the node is up
* and running (it might take some time before
* the node is actually ready even when the client
* is connected).
*
* We check that the `parity_enode` RPC calls
* returns successfully
*/
_waitUntilNodeReady (_timeleft) {
// Default timeout to 30 seconds
const timeleft = Number.isFinite(_timeleft)
? _timeleft
: 30 * 1000;
// After timeout, just resolve the promise...
if (timeleft <= 0) {
console.warn('node is still not ready after 30 seconds...');
return Promise.resolve(true);
}
const start = Date.now();
return this
.parity.enode()
.then(() => true)
.catch((error) => {
if (!error) {
return true;
}
if (error.type !== 'NETWORK_DISABLED') {
throw error;
}
// Timeout between 250ms and 750ms
const timeout = Math.floor(250 + (500 * Math.random()));
log.debug('waiting until node is ready', 'retry in', timeout, 'ms');
// Retry in a few...
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const duration = Date.now() - start;
this._waitUntilNodeReady(timeleft - duration).then(resolve).catch(reject);
}, timeout);
});
});
}
}

View File

@@ -27,6 +27,7 @@
transition: transform ease-out 0.1s;
transform: scale(1);
overflow: hidden;
&.copied {
animation-duration: 0.25s;

View File

@@ -17,7 +17,6 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import unknownImage from '../../../assets/images/contracts/unknown-64x64.png';
import styles from './balance.css';
@@ -63,11 +62,15 @@ class Balance extends Component {
value = api.util.fromWei(balance.value).toFormat(3);
}
let imagesrc = token.image;
if (!imagesrc) {
imagesrc = images[token.address]
? `${api.dappsUrl}${images[token.address]}`
: unknownImage;
const imageurl = token.image || images[token.address];
let imagesrc = unknownImage;
if (imageurl) {
const host = /^(\/)?api/.test(imageurl)
? api.dappsUrl
: '';
imagesrc = `${host}${imageurl}`;
}
return (
@@ -107,11 +110,7 @@ function mapStateToProps (state) {
return { images };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(Balance);

View File

@@ -17,7 +17,6 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import styles from './blockStatus.css';
@@ -113,11 +112,7 @@ function mapStateToProps (state) {
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(BlockStatus);

View File

@@ -15,6 +15,22 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.outerInput {
display: flex;
flex-direction: row;
position: relative;
.input {
flex: 1;
}
.loader {
position: absolute;
bottom: 1rem;
right: 9rem;
}
}
.input {
box-sizing: border-box;
appearance: textfield;
@@ -58,13 +74,13 @@
}
.label {
margin: 1rem 2.5rem 0.25em;
margin: 1rem 0.5rem 0.25em;
color: rgba(255, 255, 255, 0.498039);
}
.underline {
position: relative;
margin: 0 9rem 0 2.5rem;
margin: 0 0.5rem 0 0.5rem;
}
.empty {
@@ -78,7 +94,7 @@
.input {
font-size: 1.5em;
padding: 0 9rem 0.5em 2.5rem;
padding: 0 9rem 0.5em 0.5rem;
display: block;
padding-right: 6rem;
@@ -92,7 +108,7 @@
flex-direction: row;
justify-content: flex-start;
margin: 2rem 2rem 0;
margin: 2rem 0 0;
> * {
flex: 1;
@@ -107,8 +123,11 @@
.title {
text-transform: uppercase;
font-size: 1.5em;
font-color: white;
h3 {
margin: 0;
}
}
.cards {

View File

@@ -25,6 +25,7 @@ import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
import AccountCard from '~/ui/AccountCard';
import InputAddress from '~/ui/Form/InputAddress';
import Loading from '~/ui/Loading';
import Portal from '~/ui/Portal';
import { nodeOrStringProptype } from '~/util/proptypes';
import { validateAddress } from '~/util/validation';
@@ -130,7 +131,7 @@ class AddressSelect extends Component {
const input = (
<InputAddress
accountsInfo={ accountsInfo }
allowCopy={ allowCopy }
allowCopy={ (disabled || readOnly) && allowCopy ? allowCopy : false }
className={ className }
disabled={ disabled || readOnly }
error={ error }
@@ -182,17 +183,18 @@ class AddressSelect extends Component {
<label className={ styles.label } htmlFor={ id }>
{ label }
</label>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
<div className={ styles.outerInput }>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
{ this.renderLoader() }
</div>
<div className={ styles.underline }>
<TextFieldUnderline
@@ -210,6 +212,19 @@ class AddressSelect extends Component {
);
}
renderLoader () {
if (!this.store.loading) {
return null;
}
return (
<Loading
className={ styles.loader }
size={ 0.5 }
/>
);
}
renderCurrentInput () {
const { inputValue } = this.state;
@@ -304,7 +319,9 @@ class AddressSelect extends Component {
return (
<div className={ styles.category } key={ `${key}_${index}` }>
<div className={ styles.title }>{ label }</div>
<div className={ styles.title }>
<h3>{ label }</h3>
</div>
{ content }
</div>
);

View File

@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { observable, action } from 'mobx';
import { observable, action, transaction } from 'mobx';
import { flatMap, uniqBy } from 'lodash';
import { FormattedMessage } from 'react-intl';
@@ -26,6 +26,7 @@ const ZERO = /^(0x)?0*$/;
export default class AddressSelectStore {
@observable loading = false;
@observable values = [];
@observable registryValues = [];
@@ -85,7 +86,7 @@ export default class AddressSelectStore {
return emailVerification
.instance
.reverse
.call({}, [ sha3(email) ])
.call({}, [ sha3.text(email) ])
.then((address) => {
return {
address,
@@ -109,7 +110,7 @@ export default class AddressSelectStore {
this.regLookups.push((name) => {
return registryInstance
.getAddress
.call({}, [ sha3(name), 'A' ])
.call({}, [ sha3.text(name), 'A' ])
.then((address) => {
return {
address,
@@ -224,21 +225,28 @@ export default class AddressSelectStore {
};
});
// Registries Lookup
this.registryValues = [];
// Clear the previous results after 50ms
// if still fetching
const timeoutId = setTimeout(() => {
transaction(() => {
this.registryValues = [];
this.loading = true;
});
}, 50);
const lookups = this.regLookups.map((regLookup) => regLookup(value));
Promise
// Registries Lookup
return Promise
.all(lookups)
.then((results) => {
return results
.filter((result) => result && !ZERO.test(result.address));
})
.then((results) => {
results = uniqBy(results, (result) => result.address);
clearTimeout(timeoutId);
this.registryValues = results
const registryValues = uniqBy(results, (result) => result.address)
.map((result) => {
const lowercaseAddress = result.address.toLowerCase();
@@ -253,6 +261,11 @@ export default class AddressSelectStore {
return result;
});
transaction(() => {
this.loading = false;
this.registryValues = registryValues;
});
});
}

View File

@@ -16,7 +16,6 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import util from '~/api/util';
import { nodeOrStringProptype } from '~/util/proptypes';
@@ -58,7 +57,7 @@ class InputAddress extends Component {
render () {
const { accountsInfo, allowCopy, className, disabled, error, focused, hint } = this.props;
const { hideUnderline, label, onClick, onFocus, onSubmit, readOnly, small } = this.props;
const { hideUnderline, label, onClick, onFocus, readOnly, small } = this.props;
const { tabIndex, text, tokens, value } = this.props;
const account = value && (accountsInfo[value] || tokens[value]);
@@ -77,7 +76,7 @@ class InputAddress extends Component {
const props = {};
if (!readOnly && !disabled) {
if (!disabled) {
props.focused = focused;
}
@@ -91,10 +90,10 @@ class InputAddress extends Component {
hideUnderline={ hideUnderline }
hint={ hint }
label={ label }
onChange={ this.handleInputChange }
onChange={ this.onChange }
onClick={ onClick }
onFocus={ onFocus }
onSubmit={ onSubmit }
onSubmit={ this.onSubmit }
readOnly={ readOnly }
tabIndex={ tabIndex }
value={
@@ -133,22 +132,34 @@ class InputAddress extends Component {
return (
<div className={ classes.join(' ') }>
<IdentityIcon
inline center
address={ value } />
address={ value }
center
inline />
</div>
);
}
handleInputChange = (event, value) => {
const isEmpty = (value.length === 0);
onChange = (event, _value) => {
let address = _value.trim();
const isEmpty = (address.length === 0);
this.setState({ isEmpty });
if (!/^0x/.test(value) && util.isAddressValid(`0x${value}`)) {
return this.props.onChange(event, `0x${value}`);
}
if (this.props.onChange) {
if (!/^0x/.test(address) && util.isAddressValid(`0x${address}`)) {
address = `0x${address}`;
}
this.props.onChange(event, value);
this.props.onChange(event, address);
}
}
onSubmit = (_value) => {
const address = _value.trim();
if (this.props.onSubmit) {
this.props.onSubmit(address);
}
}
}
@@ -162,11 +173,7 @@ function mapStateToProps (state) {
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(InputAddress);

View File

@@ -16,7 +16,6 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import AddressSelect from '../AddressSelect';
@@ -68,11 +67,7 @@ function mapStateToProps (state) {
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(InputAddressSelect);

View File

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

View File

@@ -0,0 +1,31 @@
/* Copyright 2015, 2016 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.strength {
margin-top: 1.25em;
}
.feedback {
font-size: 0.75em;
}
.label {
user-select: none;
line-height: 18px;
font-size: 12px;
color: rgba(255, 255, 255, 0.498039);
}

View File

@@ -0,0 +1,125 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { debounce } from 'lodash';
import { LinearProgress } from 'material-ui';
import { FormattedMessage } from 'react-intl';
import zxcvbn from 'zxcvbn';
import styles from './passwordStrength.css';
const BAR_STYLE = {
borderRadius: 1,
height: 7,
marginTop: '0.5em'
};
export default class PasswordStrength extends Component {
static propTypes = {
input: PropTypes.string.isRequired
};
state = {
strength: null
};
constructor (props) {
super(props);
this.updateStrength = debounce(this._updateStrength, 50, { leading: true });
}
componentWillMount () {
this.updateStrength(this.props.input);
}
componentWillReceiveProps (nextProps) {
if (nextProps.input !== this.props.input) {
this.updateStrength(nextProps.input);
}
}
_updateStrength (input = '') {
const strength = zxcvbn(input);
this.setState({ strength });
}
render () {
const { strength } = this.state;
if (!strength) {
return null;
}
const { score, feedback } = strength;
// Score is between 0 and 4
const value = score * 100 / 5 + 20;
const color = this.getStrengthBarColor(score);
return (
<div className={ styles.strength }>
<label className={ styles.label }>
<FormattedMessage
id='ui.passwordStrength.label'
defaultMessage='password strength'
/>
</label>
<LinearProgress
color={ color }
mode='determinate'
style={ BAR_STYLE }
value={ value }
/>
<div className={ styles.feedback }>
{ this.renderFeedback(feedback) }
</div>
</div>
);
}
// Note that the suggestions are in english, thus it wouldn't
// make sense to add translations to surrounding words
renderFeedback (feedback = {}) {
const { suggestions = [] } = feedback;
return (
<div>
<p>
{ suggestions.join(' ') }
</p>
</div>
);
}
getStrengthBarColor (score) {
switch (score) {
case 4:
case 3:
return 'lightgreen';
case 2:
return 'yellow';
case 1:
return 'orange';
default:
return 'red';
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import PasswordStrength from './passwordStrength';
const INPUT_A = 'l33t_test';
const INPUT_B = 'Fu£dk;s$%kdlaOe9)_';
const INPUT_NULL = '';
function render (props) {
return shallow(
<PasswordStrength { ...props } />
).shallow();
}
describe('ui/Form/PasswordStrength', () => {
describe('rendering', () => {
it('renders', () => {
expect(render({ input: INPUT_A })).to.be.ok;
});
it('renders a linear progress', () => {
expect(render({ input: INPUT_A }).find('LinearProgress')).to.be.ok;
});
describe('compute strength', () => {
it('has low score with empty input', () => {
expect(
render({ input: INPUT_NULL }).find('LinearProgress').props().value
).to.equal(20);
});
it('has medium score', () => {
expect(
render({ input: INPUT_A }).find('LinearProgress').props().value
).to.equal(60);
});
it('has high score', () => {
expect(
render({ input: INPUT_B }).find('LinearProgress').props().value
).to.equal(100);
});
});
});
});

View File

@@ -71,7 +71,9 @@ export default class TypedInput extends Component {
const { isEth, value } = this.props;
if (typeof isEth === 'boolean' && value) {
const ethValue = isEth ? fromWei(value) : value;
// Remove formatting commas
const sanitizedValue = typeof value === 'string' ? value.replace(/,/g, '') : value;
const ethValue = isEth ? fromWei(sanitizedValue) : value;
this.setState({ isEth, ethValue });
}
}
@@ -393,7 +395,9 @@ export default class TypedInput extends Component {
return this.setState({ isEth: !isEth });
}
const value = isEth ? toWei(ethValue) : fromWei(ethValue);
// Remove formatting commas
const sanitizedValue = typeof ethValue === 'string' ? ethValue.replace(/,/g, '') : ethValue;
const value = isEth ? toWei(sanitizedValue) : fromWei(sanitizedValue);
this.setState({ isEth: !isEth, ethValue: value }, () => {
this.onEthValueChange(null, value);
});

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