diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbc310ac..2b70dacf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Current Master +## 2.0.0 2016-05-23 + +- UI Overhaul per Vlad Todirut's designs. +- Replaced identicons with jazzicons. +- Fixed glitchy transitions. +- Added support for capitalization-based address checksums. +- Send value is no longer limited by javascript number precision, and is always in ETH. +- Added ability to generate new accounts. +- Added ability to locally nickname accounts. + ## 1.8.4 2016-05-13 - Point rpc servers to https endpoints. diff --git a/app/fonts/Transat Black/transat_black-webfont.eot b/app/fonts/Transat Black/transat_black-webfont.eot new file mode 100755 index 000000000..268ba943d Binary files /dev/null and b/app/fonts/Transat Black/transat_black-webfont.eot differ diff --git a/app/fonts/Transat Black/transat_black-webfont.svg b/app/fonts/Transat Black/transat_black-webfont.svg new file mode 100755 index 000000000..b8762f7d6 --- /dev/null +++ b/app/fonts/Transat Black/transat_black-webfont.svg @@ -0,0 +1,2592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/Transat Black/transat_black-webfont.ttf b/app/fonts/Transat Black/transat_black-webfont.ttf new file mode 100755 index 000000000..d501f77da Binary files /dev/null and b/app/fonts/Transat Black/transat_black-webfont.ttf differ diff --git a/app/fonts/Transat Black/transat_black-webfont.woff b/app/fonts/Transat Black/transat_black-webfont.woff new file mode 100755 index 000000000..ea5d95b40 Binary files /dev/null and b/app/fonts/Transat Black/transat_black-webfont.woff differ diff --git a/app/fonts/Transat Black/transat_black-webfont.woff2 b/app/fonts/Transat Black/transat_black-webfont.woff2 new file mode 100755 index 000000000..64113a718 Binary files /dev/null and b/app/fonts/Transat Black/transat_black-webfont.woff2 differ diff --git a/app/fonts/Transat Light/transat_light-webfont.eot b/app/fonts/Transat Light/transat_light-webfont.eot new file mode 100755 index 000000000..1660cdad9 Binary files /dev/null and b/app/fonts/Transat Light/transat_light-webfont.eot differ diff --git a/app/fonts/Transat Light/transat_light-webfont.svg b/app/fonts/Transat Light/transat_light-webfont.svg new file mode 100755 index 000000000..82327f942 --- /dev/null +++ b/app/fonts/Transat Light/transat_light-webfont.svg @@ -0,0 +1,2399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/Transat Light/transat_light-webfont.ttf b/app/fonts/Transat Light/transat_light-webfont.ttf new file mode 100755 index 000000000..dda8bb83f Binary files /dev/null and b/app/fonts/Transat Light/transat_light-webfont.ttf differ diff --git a/app/fonts/Transat Light/transat_light-webfont.woff b/app/fonts/Transat Light/transat_light-webfont.woff new file mode 100755 index 000000000..2e898fefc Binary files /dev/null and b/app/fonts/Transat Light/transat_light-webfont.woff differ diff --git a/app/fonts/Transat Light/transat_light-webfont.woff2 b/app/fonts/Transat Light/transat_light-webfont.woff2 new file mode 100755 index 000000000..b2dfed9ad Binary files /dev/null and b/app/fonts/Transat Light/transat_light-webfont.woff2 differ diff --git a/app/fonts/Transat Medium/transat_medium-webfont.eot b/app/fonts/Transat Medium/transat_medium-webfont.eot new file mode 100755 index 000000000..03a1b8339 Binary files /dev/null and b/app/fonts/Transat Medium/transat_medium-webfont.eot differ diff --git a/app/fonts/Transat Medium/transat_medium-webfont.svg b/app/fonts/Transat Medium/transat_medium-webfont.svg new file mode 100755 index 000000000..073fe602f --- /dev/null +++ b/app/fonts/Transat Medium/transat_medium-webfont.svg @@ -0,0 +1,2813 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/Transat Medium/transat_medium-webfont.ttf b/app/fonts/Transat Medium/transat_medium-webfont.ttf new file mode 100755 index 000000000..361df5c16 Binary files /dev/null and b/app/fonts/Transat Medium/transat_medium-webfont.ttf differ diff --git a/app/fonts/Transat Medium/transat_medium-webfont.woff b/app/fonts/Transat Medium/transat_medium-webfont.woff new file mode 100755 index 000000000..fd3616d48 Binary files /dev/null and b/app/fonts/Transat Medium/transat_medium-webfont.woff differ diff --git a/app/fonts/Transat Medium/transat_medium-webfont.woff2 b/app/fonts/Transat Medium/transat_medium-webfont.woff2 new file mode 100755 index 000000000..d6c99d7fb Binary files /dev/null and b/app/fonts/Transat Medium/transat_medium-webfont.woff2 differ diff --git a/app/fonts/Transat Standard/transat_standard-webfont.eot b/app/fonts/Transat Standard/transat_standard-webfont.eot new file mode 100755 index 000000000..8bbc5a3e1 Binary files /dev/null and b/app/fonts/Transat Standard/transat_standard-webfont.eot differ diff --git a/app/fonts/Transat Standard/transat_standard-webfont.svg b/app/fonts/Transat Standard/transat_standard-webfont.svg new file mode 100755 index 000000000..72d184d1a --- /dev/null +++ b/app/fonts/Transat Standard/transat_standard-webfont.svg @@ -0,0 +1,2827 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/fonts/Transat Standard/transat_standard-webfont.ttf b/app/fonts/Transat Standard/transat_standard-webfont.ttf new file mode 100755 index 000000000..ccea33a5f Binary files /dev/null and b/app/fonts/Transat Standard/transat_standard-webfont.ttf differ diff --git a/app/fonts/Transat Standard/transat_standard-webfont.woff b/app/fonts/Transat Standard/transat_standard-webfont.woff new file mode 100755 index 000000000..8e3abeaa5 Binary files /dev/null and b/app/fonts/Transat Standard/transat_standard-webfont.woff differ diff --git a/app/fonts/Transat Standard/transat_standard-webfont.woff2 b/app/fonts/Transat Standard/transat_standard-webfont.woff2 new file mode 100755 index 000000000..3d62b0ffd Binary files /dev/null and b/app/fonts/Transat Standard/transat_standard-webfont.woff2 differ diff --git a/app/manifest.json b/app/manifest.json index d9697627f..56f0ce62e 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "Metamask", - "version": "1.8.4", + "version": "2.0.0", "manifest_version": 2, "description": "__MSG_appDescription__", "icons": { diff --git a/app/scripts/background.js b/app/scripts/background.js index a52eab2d3..8c6adff04 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,11 +1,12 @@ const Dnode = require('dnode') -const ObjectMultiplex = require('./lib/obj-multiplex') const eos = require('end-of-stream') const combineStreams = require('pumpify') const extend = require('xtend') const EthStore = require('eth-store') -const PortStream = require('./lib/port-stream.js') const MetaMaskProvider = require('web3-provider-engine/zero.js') +const handleRequestsFromStream = require('web3-stream-provider/handler') +const ObjectMultiplex = require('./lib/obj-multiplex') +const PortStream = require('./lib/port-stream.js') const IdentityStore = require('./lib/idStore') const createTxNotification = require('./lib/notifications.js').createTxNotification const createMsgNotification = require('./lib/notifications.js').createMsgNotification @@ -132,25 +133,6 @@ function storeSetFromObj(store, obj){ } - -// handle rpc requests -function onRpcRequest(remoteStream, payload){ - // console.log('MetaMaskPlugin - incoming payload:', payload) - provider.sendAsync(payload, function onPayloadHandled(err, response){ - // provider engine errors are included in response objects - if (!payload.isMetamaskInternal) { - console.log('MetaMaskPlugin - RPC complete:', payload, '->', response) - if (response.error) console.error('Error in RPC response:\n'+response.error.message) - } - try { - remoteStream.write(response) - } catch (err) { - console.error(err) - } - }) -} - - // // remote features // @@ -161,7 +143,15 @@ function setupPublicConfig(stream){ } function setupProviderConnection(stream){ - stream.on('data', onRpcRequest.bind(null, stream)) + handleRequestsFromStream(stream, provider, logger) + + function logger(err, request, response){ + if (err) return console.error(err.stack) + if (!request.isMetamaskInternal) { + console.log('MetaMaskPlugin - RPC complete:', request, '->', response) + if (response.error) console.error('Error in RPC response:\n'+response.error.message) + } + } } function setupControllerConnection(stream){ @@ -182,6 +172,8 @@ function setupControllerConnection(stream){ setLocked: idStore.setLocked.bind(idStore), clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore), exportAccount: idStore.exportAccount.bind(idStore), + revealAccount: idStore.revealAccount.bind(idStore), + saveAccountLabel: idStore.saveAccountLabel.bind(idStore), }) stream.pipe(dnode).pipe(stream) dnode.on('remote', function(remote){ diff --git a/app/scripts/config.js b/app/scripts/config.js new file mode 100644 index 000000000..f26e6778d --- /dev/null +++ b/app/scripts/config.js @@ -0,0 +1,12 @@ +const MAINET_RPC_URL = 'https://mainnet.infura.io/' +const TESTNET_RPC_URL = 'https://morden.infura.io/' +const DEFAULT_RPC_URL = TESTNET_RPC_URL + +module.exports = { + network: { + default: DEFAULT_RPC_URL, + mainnet: MAINET_RPC_URL, + testnet: TESTNET_RPC_URL, + }, +} + diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 1b7b98ec9..43ae5bc98 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -25,14 +25,14 @@ pluginStream.on('error', console.error.bind(console)) // forward communication plugin->inpage pageStream.pipe(pluginStream).pipe(pageStream) -// connect contentscript->inpage control stream +// connect contentscript->inpage reload stream var mx = ObjectMultiplex() mx.on('error', console.error.bind(console)) mx.pipe(pageStream) -var controlStream = mx.createStream('control') -controlStream.on('error', console.error.bind(console)) +var reloadStream = mx.createStream('reload') +reloadStream.on('error', console.error.bind(console)) // if we lose connection with the plugin, trigger tab refresh pluginStream.on('close', function(){ - controlStream.write({ method: 'reset' }) + reloadStream.write({ method: 'reset' }) }) \ No newline at end of file diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 33e2c9358..b8532747e 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,18 +1,12 @@ cleanContextForImports() -const createPayload = require('web3-provider-engine/util/create-payload') -const StreamProvider = require('./lib/stream-provider.js') -const LocalMessageDuplexStream = require('./lib/local-message-stream.js') -const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex -const RemoteStore = require('./lib/remote-store.js').RemoteStore const Web3 = require('web3') -const once = require('once') +const LocalMessageDuplexStream = require('./lib/local-message-stream.js') +const setupDappAutoReload = require('./lib/auto-reload.js') +const MetamaskInpageProvider = require('./lib/inpage-provider.js') restoreContextAfterImports() -// rename on window +// remove from window delete window.Web3 -window.MetamaskWeb3 = Web3 - -const DEFAULT_RPC_URL = 'https://rpc.metamask.io/' // @@ -20,148 +14,40 @@ const DEFAULT_RPC_URL = 'https://rpc.metamask.io/' // // setup background connection -var pluginStream = new LocalMessageDuplexStream({ +var metamaskStream = new LocalMessageDuplexStream({ name: 'inpage', target: 'contentscript', }) -var mx = setupMultiplex(pluginStream) -// connect to provider -var remoteProvider = new StreamProvider() -remoteProvider.pipe(mx.createStream('provider')).pipe(remoteProvider) -remoteProvider.on('error', console.error.bind(console)) - -// subscribe to metamask public config -var initState = JSON.parse(localStorage['MetaMask-Config'] || '{}') -var publicConfigStore = new RemoteStore(initState) -var storeStream = publicConfigStore.createStream() -storeStream.pipe(mx.createStream('publicConfig')).pipe(storeStream) -publicConfigStore.subscribe(function(state){ - localStorage['MetaMask-Config'] = JSON.stringify(state) -}) +// compose the inpage provider +var inpageProvider = new MetamaskInpageProvider(metamaskStream) // // setup web3 // -var web3 = new Web3(remoteProvider) +var web3 = new Web3(inpageProvider) web3.setProvider = function(){ console.log('MetaMask - overrode web3.setProvider') } console.log('MetaMask - injected web3') // -// automatic dapp reset +// export global web3 with auto dapp reload // -// export web3 as a global, checking for usage -var pageIsUsingWeb3 = false -var resetWasRequested = false -window.web3 = ensnare(web3, once(function(){ - // if web3 usage happened after a reset request, trigger reset late - if (resetWasRequested) return triggerReset() - // mark web3 as used - pageIsUsingWeb3 = true - // reset web3 reference - window.web3 = web3 -})) - -// listen for reset requests -mx.createStream('control').once('data', function(){ - resetWasRequested = true - // ignore if web3 was not used - if (!pageIsUsingWeb3) return - // reload after short timeout - triggerReset() -}) - -function triggerReset(){ - setTimeout(function(){ - window.location.reload() - }, 500) -} - -// -// handle synchronous requests -// - -global.publicConfigStore = publicConfigStore +var reloadStream = inpageProvider.multiStream.createStream('reload') +setupDappAutoReload(web3, reloadStream) // set web3 defaultAcount -publicConfigStore.subscribe(function(state){ +inpageProvider.publicConfigStore.subscribe(function(state){ web3.eth.defaultAccount = state.selectedAddress }) -// setup sync http provider -var providerConfig = publicConfigStore.get('provider') || {} -var providerUrl = providerConfig.rpcTarget ? providerConfig.rpcTarget : DEFAULT_RPC_URL -var syncProvider = new Web3.providers.HttpProvider(providerUrl) -publicConfigStore.subscribe(function(state){ - if (!state.provider) return - if (!state.provider.rpcTarget || state.provider.rpcTarget === providerUrl) return - providerUrl = state.provider.rpcTarget - syncProvider = new Web3.providers.HttpProvider(providerUrl) -}) - -// handle sync methods -remoteProvider.send = function(payload){ - var result = null - switch (payload.method) { - - case 'eth_accounts': - // read from localStorage - var selectedAddress = publicConfigStore.get('selectedAddress') - result = selectedAddress ? [selectedAddress] : [] - break - - case 'eth_coinbase': - // read from localStorage - var selectedAddress = publicConfigStore.get('selectedAddress') - result = selectedAddress || '0x0000000000000000000000000000000000000000' - break - - // fallback to normal rpc - default: - return syncProvider.send(payload) - - } - - // return the result - return { - id: payload.id, - jsonrpc: payload.jsonrpc, - result: result, - } -} - - // // util // -// creates a proxy object that calls cb everytime the obj's properties/fns are accessed -function ensnare(obj, cb){ - var proxy = {} - Object.keys(obj).forEach(function(key){ - var val = obj[key] - switch (typeof val) { - case 'function': - proxy[key] = function(){ - cb() - val.apply(obj, arguments) - } - return - default: - Object.defineProperty(proxy, key, { - get: function(){ cb(); return obj[key] }, - set: function(val){ cb(); return obj[key] = val }, - }) - return - } - }) - return proxy -} - // need to make sure we aren't affected by overlapping namespaces // and that we dont affect the app with our namespace // mostly a fix for web3's BigNumber if AMD's "define" is defined... diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js new file mode 100644 index 000000000..95a744b2c --- /dev/null +++ b/app/scripts/lib/auto-reload.js @@ -0,0 +1,37 @@ +const once = require('once') +const ensnare = require('./ensnare.js') + +module.exports = setupDappAutoReload + + +function setupDappAutoReload(web3, controlStream){ + + // export web3 as a global, checking for usage + var pageIsUsingWeb3 = false + var resetWasRequested = false + global.web3 = ensnare(web3, once(function(){ + // if web3 usage happened after a reset request, trigger reset late + if (resetWasRequested) return triggerReset() + // mark web3 as used + pageIsUsingWeb3 = true + // reset web3 reference + global.web3 = web3 + })) + + // listen for reset requests from metamask + controlStream.once('data', function(){ + resetWasRequested = true + // ignore if web3 was not used + if (!pageIsUsingWeb3) return + // reload after short timeout + triggerReset() + }) + + // reload the page + function triggerReset(){ + setTimeout(function(){ + global.location.reload() + }, 500) + } + +} \ No newline at end of file diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 3c9326db9..f5e1cf38d 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,11 +1,12 @@ const Migrator = require('pojo-migrator') const extend = require('xtend') +const MetamaskConfig = require('../config.js') +const migrations = require('./migrations') const STORAGE_KEY = 'metamask-config' -const TESTNET_RPC = 'https://morden.infura.io' -const MAINNET_RPC = 'https://mainnet.infura.io/' +const TESTNET_RPC = MetamaskConfig.network.testnet +const MAINNET_RPC = MetamaskConfig.network.mainnet -const migrations = require('./migrations') /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -229,6 +230,26 @@ ConfigManager.prototype.updateTx = function(tx) { this._saveTxList(transactions) } +// wallet nickname methods + +ConfigManager.prototype.getWalletNicknames = function() { + var data = this.getData() + let nicknames = ('walletNicknames' in data) ? data.walletNicknames : {} + return nicknames +} + +ConfigManager.prototype.nicknameForWallet = function(account) { + let nicknames = this.getWalletNicknames() + return nicknames[account] +} + +ConfigManager.prototype.setNicknameForWallet = function(account, nickname) { + let nicknames = this.getWalletNicknames() + nicknames[account] = nickname + var data = this.getData() + data.walletNicknames = nicknames + this.setData(data) +} // observable diff --git a/app/scripts/lib/ensnare.js b/app/scripts/lib/ensnare.js new file mode 100644 index 000000000..b70330a5a --- /dev/null +++ b/app/scripts/lib/ensnare.js @@ -0,0 +1,24 @@ +module.exports = ensnare + +// creates a proxy object that calls cb everytime the obj's properties/fns are accessed +function ensnare(obj, cb){ + var proxy = {} + Object.keys(obj).forEach(function(key){ + var val = obj[key] + switch (typeof val) { + case 'function': + proxy[key] = function(){ + cb() + val.apply(obj, arguments) + } + return + default: + Object.defineProperty(proxy, key, { + get: function(){ cb(); return obj[key] }, + set: function(val){ cb(); return obj[key] = val }, + }) + return + } + }) + return proxy +} diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index b8d825d8b..9d2552e8b 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -105,14 +105,29 @@ IdentityStore.prototype.getSelectedAddress = function(){ return configManager.getSelectedAccount() } -IdentityStore.prototype.setSelectedAddress = function(address){ +IdentityStore.prototype.setSelectedAddress = function(address, cb){ if (!address) { var addresses = this._getAddresses() address = addresses[0] } configManager.setSelectedAccount(address) + if (cb) return cb(null, address) +} + +IdentityStore.prototype.revealAccount = function(cb) { + let addresses = this._getAddresses() + const derivedKey = this._idmgmt.derivedKey + const keyStore = this._keyStore + + keyStore.setDefaultHdDerivationPath(this.hdPathString) + keyStore.generateNewAddress(derivedKey, 1) + configManager.setWallet(keyStore.serialize()) + + addresses = this._getAddresses() + this._loadIdentities() this._didUpdate() + cb(null) } IdentityStore.prototype.getNetwork = function(tries) { @@ -310,9 +325,10 @@ IdentityStore.prototype._loadIdentities = function(){ // // add to ethStore this._ethStore.addAccount(address) // add to identities + const defaultLabel = 'Wallet ' + (i+1) + const nickname = configManager.nicknameForWallet(address) var identity = { - name: 'Wallet ' + (i+1), - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + name: nickname || defaultLabel, address: address, mayBeFauceting: this._mayBeFauceting(i), } @@ -321,6 +337,13 @@ IdentityStore.prototype._loadIdentities = function(){ this._didUpdate() } +IdentityStore.prototype.saveAccountLabel = function(account, label, cb) { + configManager.setNicknameForWallet(account, label) + this._loadIdentities() + cb(null, label) + this._didUpdate() +} + // mayBeFauceting // If on testnet, index 0 may be fauceting. // The UI will have to check the balance to know. diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js new file mode 100644 index 000000000..70b0d80dd --- /dev/null +++ b/app/scripts/lib/inpage-provider.js @@ -0,0 +1,123 @@ +const HttpProvider = require('web3/lib/web3/httpprovider') +const Streams = require('mississippi') +const ObjectMultiplex = require('./obj-multiplex') +const StreamProvider = require('web3-stream-provider') +const RemoteStore = require('./remote-store.js').RemoteStore +const MetamaskConfig = require('../config.js') + +module.exports = MetamaskInpageProvider + + +function MetamaskInpageProvider(connectionStream){ + const self = this + + // setup connectionStream multiplexing + var multiStream = ObjectMultiplex() + Streams.pipe(connectionStream, multiStream, connectionStream, function(err){ + console.warn('MetamaskInpageProvider - lost connection to MetaMask') + if (err) throw err + }) + self.multiStream = multiStream + + // subscribe to metamask public config + var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config') + var storeStream = publicConfigStore.createStream() + Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function(err){ + console.warn('MetamaskInpageProvider - lost connection to MetaMask publicConfig') + if (err) throw err + }) + self.publicConfigStore = publicConfigStore + + // connect to sync provider + self.syncProvider = createSyncProvider(publicConfigStore.get('provider')) + // subscribe to publicConfig to update the syncProvider on change + publicConfigStore.subscribe(function(state){ + self.syncProvider = createSyncProvider(state.provider) + }) + + // connect to async provider + var asyncProvider = new StreamProvider() + Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function(err){ + console.warn('MetamaskInpageProvider - lost connection to MetaMask provider') + if (err) throw err + }) + asyncProvider.on('error', console.error.bind(console)) + self.asyncProvider = asyncProvider + // overwrite own sendAsync method + self.sendAsync = asyncProvider.sendAsync.bind(asyncProvider) +} + +MetamaskInpageProvider.prototype.send = function(payload){ + const self = this + + var result = null + switch (payload.method) { + + case 'eth_accounts': + // read from localStorage + var selectedAddress = self.publicConfigStore.get('selectedAddress') + result = selectedAddress ? [selectedAddress] : [] + break + + case 'eth_coinbase': + // read from localStorage + var selectedAddress = self.publicConfigStore.get('selectedAddress') + result = selectedAddress || '0x0000000000000000000000000000000000000000' + break + + // fallback to normal rpc + default: + return self.syncProvider.send(payload) + + } + + // return the result + return { + id: payload.id, + jsonrpc: payload.jsonrpc, + result: result, + } +} + +MetamaskInpageProvider.prototype.sendAsync = function(){ + throw new Error('MetamaskInpageProvider - sendAsync not overwritten') +} + +MetamaskInpageProvider.prototype.isConnected = function(){ + return true +} + +// util + +function createSyncProvider(providerConfig){ + providerConfig = providerConfig || {} + var syncProviderUrl = undefined + + if (providerConfig.rpcTarget) { + syncProviderUrl = providerConfig.rpcTarget + } else { + switch(providerConfig.type) { + case 'testnet': + syncProviderUrl = MetamaskConfig.network.testnet + break + case 'mainnet': + syncProviderUrl = MetamaskConfig.network.mainnet + break + default: + syncProviderUrl = MetamaskConfig.network.default + } + } + return new HttpProvider(syncProviderUrl) +} + +function remoteStoreWithLocalStorageCache(storageKey){ + // read local cache + var initState = JSON.parse(localStorage[storageKey] || '{}') + var store = new RemoteStore(initState) + // cache the latest state locally + store.subscribe(function(state){ + localStorage[storageKey] = JSON.stringify(state) + }) + + return store +} \ No newline at end of file diff --git a/app/scripts/lib/stream-provider.js b/app/scripts/lib/stream-provider.js deleted file mode 100644 index 505e45d1f..000000000 --- a/app/scripts/lib/stream-provider.js +++ /dev/null @@ -1,72 +0,0 @@ -const Duplex = require('readable-stream').Duplex -const inherits = require('util').inherits - -module.exports = StreamProvider - - -inherits(StreamProvider, Duplex) - -function StreamProvider(){ - Duplex.call(this, { - objectMode: true, - }) - - this._payloads = {} -} - -// public - -StreamProvider.prototype.send = function(payload){ - throw new Error('StreamProvider - does not support synchronous RPC calls. called: "'+payload.method+'"') -} - -StreamProvider.prototype.sendAsync = function(payload, callback){ - // console.log('StreamProvider - sending payload', payload) - var id = payload.id - if (Array.isArray(payload)) { - id = 'batch'+payload[0].id - } - this._payloads[id] = [payload, callback] - // console.log('payload for plugin:', payload) - this.push(payload) -} - -StreamProvider.prototype.isConnected = function(){ - return true -} - -// private - -StreamProvider.prototype._onResponse = function(response){ - // console.log('StreamProvider - got response', payload) - var id = response.id - if (Array.isArray(response)) { - id = 'batch'+response[0].id - } - var data = this._payloads[id] - if (!data) throw new Error('StreamProvider - Unknown response id') - delete this._payloads[id] - var payload = data[0] - var callback = data[1] - - // logging - var res = Array.isArray(response) ? response : [response] - // ;(Array.isArray(payload) ? payload : [payload]).forEach(function(payload, index){ - // console.log('plugin response:', payload.id, payload.method, payload.params, '->', res[index].result) - // }) - - callback(null, response) -} - -// stream plumbing - -StreamProvider.prototype._read = noop - -StreamProvider.prototype._write = function(msg, encoding, cb){ - this._onResponse(msg) - cb() -} - -// util - -function noop(){} \ No newline at end of file diff --git a/app/scripts/popup.js b/app/scripts/popup.js index e9ca7cd71..4fa6e1127 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -7,7 +7,7 @@ const MetaMaskUi = require('../../ui') const MetaMaskUiCss = require('../../ui/css') const injectCss = require('inject-css') const PortStream = require('./lib/port-stream.js') -const StreamProvider = require('./lib/stream-provider.js') +const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex // setup app diff --git a/gulpfile.js b/gulpfile.js index c4e9630ea..2322be608 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,6 +30,10 @@ gulp.task('copy:images', copyTask({ source: './app/images/', destination: './dist/images', })) +gulp.task('copy:fonts', copyTask({ + source: './app/fonts/', + destination: './dist/fonts', +})) gulp.task('copy:reload', copyTask({ source: './app/scripts/', destination: './dist/scripts', @@ -40,7 +44,7 @@ gulp.task('copy:root', copyTask({ destination: './dist', pattern: '/*', })) -gulp.task('copy', gulp.parallel('copy:locales','copy:images','copy:reload','copy:root')) +gulp.task('copy', gulp.parallel('copy:locales','copy:images','copy:fonts','copy:reload','copy:root')) gulp.task('copy:watch', function(){ gulp.watch(['./app/{_locales,images}/', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy')) }) diff --git a/package.json b/package.json index 3bb2dd3a1..c5ec98b11 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ }, "dependencies": { "async": "^1.5.2", + "babel-preset-es2015": "^6.9.0", + "babel-register": "^6.9.0", "browserify-derequire": "^0.9.4", "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", @@ -36,26 +38,32 @@ "hat": "0.0.3", "identicon.js": "^1.2.1", "inject-css": "^0.1.1", + "jazzicon": "^1.1.3", + "menu-droppo": "^1.1.0", "metamask-logo": "^1.1.5", + "mississippi": "^1.2.0", "multiplex": "^6.7.0", "once": "^1.3.3", "pojo-migrator": "^2.1.0", "polyfill-crypto.getrandomvalues": "^1.0.0", "pumpify": "^1.3.4", - "react": "^0.14.3", - "react-addons-css-transition-group": "^0.14.7", - "react-dom": "^0.14.3", + "react": "^15.0.2", + "react-addons-css-transition-group": "^15.0.2", + "react-dom": "^15.0.2", "react-hyperscript": "^2.2.2", - "react-redux": "^4.0.3", + "react-redux": "^4.4.5", "readable-stream": "^2.1.2", "redux": "^3.0.5", "redux-logger": "^2.3.1", "redux-thunk": "^1.0.2", + "sandwich-expando": "^1.0.5", "textarea-caret": "^3.0.1", "three.js": "^0.73.2", "through2": "^2.0.1", + "vreme": "^3.0.2", "web3": "ethereum/web3.js#0.16.0", - "web3-provider-engine": "^7.6.3", + "web3-provider-engine": "^7.6.5", + "web3-stream-provider": "^2.0.1", "xtend": "^4.0.1" }, "devDependencies": { diff --git a/svg-notifications.md b/svg-notifications.md index bbb74da79..fd3b63f7a 100644 --- a/svg-notifications.md +++ b/svg-notifications.md @@ -12,6 +12,9 @@ Heres some utilities for preparing the data uri: build a template using pure svg: +generate uri +'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svgSrc) + diff --git a/test/unit/actions/restore_vault_test.js b/test/unit/actions/restore_vault_test.js index 5675028b1..609f5429e 100644 --- a/test/unit/actions/restore_vault_test.js +++ b/test/unit/actions/restore_vault_test.js @@ -21,7 +21,13 @@ describe('#recoverFromSeed(password, seed)', function() { // stub out account manager actions._setAccountManager({ - recoverFromSeed(pw, seed, cb) { cb(null, [{}, {}]) }, + recoverFromSeed(pw, seed, cb) { + cb(null, { + identities: { + foo: 'bar' + } + }) + }, }) it('sets metamask.isUnlocked to true', function() { diff --git a/test/unit/actions/save_account_label_test.js b/test/unit/actions/save_account_label_test.js new file mode 100644 index 000000000..1df428b1d --- /dev/null +++ b/test/unit/actions/save_account_label_test.js @@ -0,0 +1,36 @@ +var jsdom = require('mocha-jsdom') +var assert = require('assert') +var freeze = require('deep-freeze-strict') +var path = require('path') + +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) + +describe('SAVE_ACCOUNT_LABEL', function() { + + it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function() { + var initialState = { + metamask: { + identities: { + foo: { + name: 'bar' + } + }, + } + } + freeze(initialState) + + const action = { + type: actions.SAVE_ACCOUNT_LABEL, + value: { + account: 'foo', + label: 'baz' + }, + } + freeze(action) + + var resultingState = reducers(initialState, action) + assert.equal(resultingState.metamask.identities.foo.name, action.value.label) + }); +}); + diff --git a/test/unit/actions/set_selected_account_test.js b/test/unit/actions/set_selected_account_test.js index 0487bc5f0..69eb11e47 100644 --- a/test/unit/actions/set_selected_account_test.js +++ b/test/unit/actions/set_selected_account_test.js @@ -26,3 +26,24 @@ describe('SET_SELECTED_ACCOUNT', function() { assert.equal(resultingState.appState.activeAddress, action.value) }); }); + +describe('SHOW_ACCOUNT_DETAIL', function() { + it('updates metamask state', function() { + var initialState = { + metamask: { + selectedAccount: 'foo' + } + } + freeze(initialState) + + const action = { + type: actions.SHOW_ACCOUNT_DETAIL, + value: 'bar', + } + freeze(action) + + var resultingState = reducers(initialState, action) + assert.equal(resultingState.metamask.selectedAccount, action.value) + assert.equal(resultingState.metamask.selectedAddress, action.value) + }) +}) diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index e414ecb9e..aa94dc385 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -54,6 +54,27 @@ describe('config-manager', function() { }) }) + describe('wallet nicknames', function() { + it('should return null when no nicknames are saved', function() { + var nick = configManager.nicknameForWallet('0x0') + assert.equal(nick, null, 'no nickname returned') + }) + + it('should persist nicknames', function() { + var account = '0x0' + var nick1 = 'foo' + var nick2 = 'bar' + configManager.setNicknameForWallet(account, nick1) + + var result1 = configManager.nicknameForWallet(account) + assert.equal(result1, nick1) + + configManager.setNicknameForWallet(account, nick2) + var result2 = configManager.nicknameForWallet(account) + assert.equal(result2, nick2) + }) + }) + describe('rpc manipulations', function() { it('changing rpc should return a different rpc', function() { var firstRpc = 'first' diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 3f46d4e9b..b091d5bc7 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -17,6 +17,53 @@ describe('util', function() { this.sinon.restore() }) + describe('addressSummary', function() { + it('should add case-sensitive checksum', function() { + var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825' + var result = util.addressSummary(address) + assert.equal(result, '0xFDEa65C8...b825') + }) + }) + + describe('isValidAddress', function() { + it('should allow 40-char non-prefixed hex', function() { + var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825' + var result = util.isValidAddress(address) + assert.ok(result) + }) + + it('should allow 42-char non-prefixed hex', function() { + var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825' + var result = util.isValidAddress(address) + assert.ok(result) + }) + + it('should not allow less non hex-prefixed', function() { + var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85' + var result = util.isValidAddress(address) + assert.ok(!result) + }) + + it('should not allow less hex-prefixed', function() { + var address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85' + var result = util.isValidAddress(address) + assert.ok(!result) + }) + + it('should recognize correct capitalized checksum', function() { + var address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825' + var result = util.isValidAddress(address) + assert.ok(result) + }) + + it('should recognize incorrect capitalized checksum', function() { + var address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825' + var result = util.isValidAddress(address) + assert.ok(!result) + }) + + }) + describe('numericBalance', function() { it('should return a BN 0 if given nothing', function() { @@ -112,8 +159,29 @@ describe('util', function() { }) }) + describe('normalizeEthStringToWei', function() { + it('should convert decimal eth to pure wei BN', function() { + var input = '1.23456789' + var output = util.normalizeEthStringToWei(input) + assert.equal(output.toString(10), '1234567890000000000') + }) + + it('should convert 1 to expected wei', function() { + var input = '1' + var output = util.normalizeEthStringToWei(input) + assert.equal(output.toString(10), ethInWei) + }) + }) + describe('#normalizeNumberToWei', function() { + it('should handle a simple use case', function() { + var input = 0.0002 + var output = util.normalizeNumberToWei(input, 'ether') + var str = output.toString(10) + assert.equal(str, '200000000000000') + }) + it('should convert a kwei number to the appropriate equivalent wei', function() { var result = util.normalizeNumberToWei(1.111, 'kwei') assert.equal(result.toString(10), '1111', 'accepts decimals') diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 2775e24fb..bae44ec85 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -5,11 +5,15 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const copyToClipboard = require('copy-to-clipboard') const actions = require('./actions') +const addressSummary = require('./util').addressSummary const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const AccountPanel = require('./components/account-panel') +const Identicon = require('./components/identicon') +const EtherBalance = require('./components/eth-balance') const transactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -30,75 +34,131 @@ function AccountDetailScreen() { } AccountDetailScreen.prototype.render = function() { - var state = this.props - var selected = state.address || Object.keys(state.accounts)[0] - var identity = state.identities[selected] - var account = state.accounts[selected] - var accountDetail = state.accountDetail - var transactions = state.transactions + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var identity = props.identities[selected] + var account = props.accounts[selected] + var accountDetail = props.accountDetail + var transactions = props.transactions return ( - h('.account-detail-section.flex-column.flex-grow', { - style: { - width: '330px', - }, - }, [ + h('.account-detail-section.flex-column.flex-grow', [ - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Account Detail'), - ]), - - // account summary, with embedded action buttons - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - key: 'accountPanel' - }), - - h('div', { + // identicon, label, balance, etc + h('.account-data-subsection.flex-column.flex-grow', { style: { - display: 'flex', - } + margin: '0 20px', + }, }, [ - h('button', { - onClick: () => { - copyToClipboard(identity.address) + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: 28, }, - }, 'COPY ADDR'), + }, [ - h('button', { - onClick: () => { - this.props.dispatch(actions.showSendPage()) - }, - }, 'SEND'), + // invisible placeholder for later + h('i.fa.fa-users.fa-lg.color-orange', { + style: { + visibility: 'hidden', + }, + }), - h('button', { - onClick: () => { - this.requestAccountExport(identity.address) + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + + // small accounts nav + h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', { + onClick: this.navigateToAccounts.bind(this), + }), + ]), + + h('.flex-center', { + style: { + height: '62px', + paddingTop: '8px', + } + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing: + h('h2.font-medium.color-forest', identity && identity.name) + ]), + ]), + + // address and getter actions + h('.flex-row.flex-space-between', { + style: { + marginBottom: 16, }, - }, 'EXPORT'), + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(selected)), + + h('i.fa.fa-download.fa-md.cursor-pointer.color-orange', { + onClick: () => this.requestAccountExport(selected), + }), + + h('i.fa.fa-qrcode.fa-md.cursor-disabled.color-orange', { + onClick: () => console.warn('QRCode not implented...'), + }), + + h('i.fa.fa-clipboard.fa-md.cursor-pointer.color-orange', { + onClick: () => copyToClipboard(ethUtil.toChecksumAddress(selected)), + }), + + ]), + + // balance + send + h('.flex-row.flex-space-between', [ + + h(EtherBalance, { + value: account && account.balance, + style: { + lineHeight: '50px', + }, + }), + + h('button', { + onClick: () => this.props.dispatch(actions.showSendPage()), + style: { + margin: 10, + }, + }, 'SEND ETH'), + + ]), + ]), + // subview (tx history, pk export confirm) h(ReactCSSTransitionGroup, { - transitionName: "main", + className: 'css-transition-group', + transitionName: 'main', transitionEnterTimeout: 300, transitionLeaveTimeout: 300, }, [ this.subview(), ]), - // transaction table - /* - h('section.flex-column', [ - h('span', 'your transaction history will go here.'), - ]), - */ + ]) ) } @@ -126,10 +186,15 @@ AccountDetailScreen.prototype.transactionList = function() { var state = this.props var transactions = state.transactions - return transactionList(transactions - .filter(tx => tx.txParams.from === state.address) - .filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion) - .sort((a, b) => b.time - a.time), state.networkVersion) + var txsToRender = transactions + // only transactions that are from the current address + .filter(tx => tx.txParams.from === state.address) + // only transactions that are on the current network + .filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion) + // sort by recency + .sort((a, b) => b.time - a.time) + + return transactionList(txsToRender, state.networkVersion) } AccountDetailScreen.prototype.navigateToAccounts = function(event){ diff --git a/ui/app/accounts.js b/ui/app/accounts.js index 16f37dc67..dbf4ee0fa 100644 --- a/ui/app/accounts.js +++ b/ui/app/accounts.js @@ -3,9 +3,13 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const extend = require('xtend') +const Identicon = require('./components/identicon') const actions = require('./actions') -const AccountPanel = require('./components/account-panel') +const EtherBalance = require('./components/eth-balance') const valuesFor = require('./util').valuesFor +const addressSummary = require('./util').addressSummary +const formatBalance = require('./util').formatBalance +const findDOMNode = require('react-dom').findDOMNode module.exports = connect(mapStateToProps)(AccountsScreen) @@ -17,6 +21,7 @@ function mapStateToProps(state) { unconfTxs: state.metamask.unconfTxs, selectedAddress: state.metamask.selectedAddress, currentDomain: state.appState.currentDomain, + scrollToBottom: state.appState.scrollToBottom, } } @@ -33,37 +38,52 @@ AccountsScreen.prototype.render = function() { var actions = { onSelect: this.onSelect.bind(this), onShowDetail: this.onShowDetail.bind(this), + revealAccount: this.onRevealAccount.bind(this), + goHome: this.goHome.bind(this), } return ( - h('.accounts-section.flex-column.flex-grow', [ + h('.accounts-section.flex-grow', [ // subtitle and nav - h('.section-title.flex-column.flex-center', [ - h('h2.page-subtitle', 'Accounts'), + h('.section-title.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: actions.goHome, + }), + h('h2.page-subtitle', 'Select Account'), ]), - // current domain - /* AUDIT - * Temporarily removed - * since accounts are currently injected - * regardless of the current domain. - */ - h('.current-domain-panel.flex-center.font-small', [ - h('span', 'Selected address is visible to all sites you visit.'), - // h('span', state.currentDomain), - ]), + h('hr.horizontal-line'), // identity selection h('section.identity-section.flex-column', { style: { - maxHeight: '290px', + height: '418px', overflowY: 'auto', overflowX: 'hidden', } }, - identityList.map(renderAccountPanel) - ), + [ + identityList.map(renderAccountPanel), + + h('hr.horizontal-line', {key: 'horizontal-line1'}), + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick:() => { + actions.revealAccount() + }, + style: { + display: 'flex', + flex: '1 0 auto', + height: '40px', + paddint: '10px', + justifyContent: 'center', + alignItems: 'center', + } + }, [ + h('i.fa.fa-chevron-down.fa-lg', {key: ''}), + ]), + ]), unconfTxList.length ? ( @@ -77,10 +97,7 @@ AccountsScreen.prototype.render = function() { ) : ( null ), - - ]) - ) function renderAccountPanel(identity){ @@ -94,7 +111,48 @@ AccountsScreen.prototype.render = function() { isSelected: false, isFauceting: isFauceting, }) - return h(AccountPanel, componentState) + const selectedClass = isSelected ? '.selected' : '' + + return ( + h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { + key: `account-panel-${identity.address}`, + style: { + flex: '1 0 auto', + }, + onClick: (event) => actions.onShowDetail(identity.address, event), + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + address: identity.address + }), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + h('span', identity.name), + h('span.font-small', addressSummary(identity.address)), + // h('span.font-small', formatBalance(account.balance)), + h(EtherBalance, { + value: account.balance, + }), + + ]), + + ]) + ) + } +} + +// If a new account was revealed, scroll to the bottom +AccountsScreen.prototype.componentDidUpdate = function(){ + const scrollToBottom = this.props.scrollToBottom + + if (scrollToBottom) { + var container = findDOMNode(this) + var scrollable = container.querySelector('.identity-section') + scrollable.scrollTop = scrollable.scrollHeight } } @@ -114,3 +172,11 @@ AccountsScreen.prototype.onShowDetail = function(address, event){ event.stopPropagation() this.props.dispatch(actions.showAccountDetail(address)) } + +AccountsScreen.prototype.onRevealAccount = function() { + this.props.dispatch(actions.revealAccount()) +} + +AccountsScreen.prototype.goHome = function() { + this.props.dispatch(actions.goHome()) +} diff --git a/ui/app/actions.js b/ui/app/actions.js index dbcf3e577..9ff05c460 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1,6 +1,11 @@ var actions = { GO_HOME: 'GO_HOME', goHome: goHome, + // menu state + TOGGLE_MENU: 'TOGGLE_MENU', + toggleMenu: toggleMenu, + SET_MENU_STATE: 'SET_MENU_STATE', + closeMenu: closeMenu, // remote state UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', updateMetamaskState: updateMetamaskState, @@ -43,6 +48,8 @@ var actions = { SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + REVEAL_ACCOUNT: 'REVEAL_ACCOUNT', + revealAccount: revealAccount, // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, @@ -52,6 +59,8 @@ var actions = { exportAccount: exportAccount, SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, // tx conf screen COMPLETED_TX: 'COMPLETED_TX', TRANSACTION_ERROR: 'TRANSACTION_ERROR', @@ -105,6 +114,21 @@ function goHome() { } } +// menu state + +function toggleMenu() { + return { + type: this.TOGGLE_MENU, + } +} + +function closeMenu() { + return { + type: this.SET_MENU_STATE, + value: false, + } +} + // async actions function tryUnlockMetamask(password) { @@ -114,7 +138,7 @@ function tryUnlockMetamask(password) { if (err) { dispatch(this.unlockFailed()) } else { - dispatch(this.unlockMetamask()) + dispatch(this.unlockMetamask(selectedAccount)) } }) } @@ -133,12 +157,12 @@ function recoverFromSeed(password, seed) { return (dispatch) => { // dispatch(this.createNewVaultInProgress()) dispatch(this.showLoadingIndication()) - _accountManager.recoverFromSeed(password, seed, (err, selectedAccount) => { + _accountManager.recoverFromSeed(password, seed, (err, metamaskState) => { dispatch(this.hideLoadingIndication()) if (err) return dispatch(this.displayWarning(err.message)) - dispatch(this.goHome()) - dispatch(this.unlockMetamask()) + var account = Object.keys(metamaskState.identities)[0] + dispatch(this.unlockMetamask(account)) }) } } @@ -155,6 +179,19 @@ function setSelectedAddress(address) { } } +function revealAccount() { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + _accountManager.revealAccount((err) => { + dispatch(this.hideLoadingIndication()) + if (err) return dispatch(this.displayWarning(err.message)) + dispatch({ + type: this.REVEAL_ACCOUNT, + }) + }) + } +} + function signMsg(msgData) { return (dispatch) => { dispatch(this.showLoadingIndication()) @@ -271,9 +308,10 @@ function unlockFailed() { } } -function unlockMetamask() { +function unlockMetamask(account) { return { type: this.UNLOCK_METAMASK, + value: account, } } @@ -297,11 +335,13 @@ function lockMetamask() { function showAccountDetail(address) { return (dispatch) => { - _accountManager.setSelectedAddress(address) - - dispatch({ - type: this.SHOW_ACCOUNT_DETAIL, - value: address, + dispatch(this.showLoadingIndication()) + _accountManager.setSelectedAddress(address, (err, address) => { + dispatch(this.hideLoadingIndication()) + dispatch({ + type: this.SHOW_ACCOUNT_DETAIL, + value: address, + }) }) } } @@ -312,19 +352,19 @@ function backToAccountDetail(address) { value: address, } } -function clearSeedWordCache() { +function clearSeedWordCache(account) { return { - type: this.CLEAR_SEED_WORD_CACHE + type: this.CLEAR_SEED_WORD_CACHE, + value: account, } } function confirmSeedWords() { return (dispatch) => { dispatch(this.showLoadingIndication()) - _accountManager.clearSeedWordCache((err, accounts) => { - dispatch(this.clearSeedWordCache()) - console.log('Seed word cache cleared.') - dispatch(this.showAccountDetail(accounts[0].address)) + _accountManager.clearSeedWordCache((err, account) => { + console.log('Seed word cache cleared. ' + account) + dispatch(this.showAccountDetail(account)) }) } } @@ -443,6 +483,22 @@ function showPrivateKey(key) { } } +function saveAccountLabel(account, label) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + _accountManager.saveAccountLabel(account, label, (err) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(this.showWarning(err.message)) + } + dispatch({ + type: this.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + function showSendPage() { return { type: this.SHOW_SEND_PAGE, diff --git a/ui/app/app.js b/ui/app/app.js index a4ce40881..511012fab 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -24,6 +24,9 @@ const ConfigScreen = require('./config') const InfoScreen = require('./info') const LoadingIndicator = require('./loading') const txHelper = require('../lib/tx-helper') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') module.exports = connect(mapStateToProps)(App) @@ -42,6 +45,7 @@ function mapStateToProps(state) { seedWords: state.metamask.seedWords, unconfTxs: state.metamask.unconfTxs, unconfMsgs: state.metamask.unconfMsgs, + menuOpen: state.appState.menuOpen, } } @@ -50,15 +54,6 @@ App.prototype.render = function() { var state = this.props var view = state.currentView.name var transForward = state.transForward - var shouldHaveFooter = true - switch (view) { - case 'restoreVault': - shouldHaveFooter = false; - case 'createVault': - shouldHaveFooter = false; - case 'createVaultComplete': - shouldHaveFooter = false; - } return ( @@ -67,16 +62,13 @@ App.prototype.render = function() { // Windows was showing a vertical scroll bar: overflow: 'hidden', } - }, - [ + }, [ h(LoadingIndicator), - // top row - h('.app-header.flex-column.flex-center', { - }, [ - h('h1', 'MetaMask'), - ]), + // app bar + this.renderAppBar(), + this.renderDropdown(), // panel content h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { @@ -86,7 +78,8 @@ App.prototype.render = function() { } }, [ h(ReactCSSTransitionGroup, { - transitionName: "main", + className: 'css-transition-group', + transitionName: 'main', transitionEnterTimeout: 300, transitionLeaveTimeout: 300, }, [ @@ -95,71 +88,148 @@ App.prototype.render = function() { ]), // footer - h('.app-footer.flex-row.flex-space-around', { + // h('.app-footer.flex-row.flex-space-around', { + // style: { + // display: shouldHaveFooter ? 'flex' : 'none', + // alignItems: 'center', + // height: '56px', + // } + // }, [ + + // // settings icon + // h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), { + // style: { + // opacity: state.isUnlocked ? '1.0' : '0.0', + // transition: 'opacity 200ms ease-in', + // //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`, + // }, + // onClick: function(ev) { + // state.dispatch(actions.showConfigPage()) + // }, + // }), + + // // toggle + // onOffToggle({ + // toggleMetamaskActive: this.toggleMetamaskActive.bind(this), + // isUnlocked: state.isUnlocked, + // }), + + // // help + // h('i.fa.fa-question.fa-lg.cursor-pointer', { + // style: { + // opacity: state.isUnlocked ? '1.0' : '0.0', + // }, + // onClick() { state.dispatch(actions.showInfoPage()) } + // }), + // ]), + + ]) + ) +} + +App.prototype.renderAppBar = function(){ + var state = this.props + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { style: { - display: shouldHaveFooter ? 'flex' : 'none', alignItems: 'center', - height: '56px', - } - }, [ + visibility: state.isUnlocked ? 'visible' : 'none', + background: state.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 1, + }, + }, state.isUnlocked && [ - // settings icon - h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), { - style: { - opacity: state.isUnlocked ? '1.0' : '0.0', - transition: 'opacity 200ms ease-in', - //transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`, - }, - onClick: function(ev) { - state.dispatch(actions.showConfigPage()) - }, + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', }), - // toggle - onOffToggle({ - toggleMetamaskActive: this.toggleMetamaskActive.bind(this), - isUnlocked: state.isUnlocked, - }), + // metamask name + h('h1', 'MetaMask'), - // help - h('i.fa.fa-question.fa-lg.cursor-pointer', { - style: { - opacity: state.isUnlocked ? '1.0' : '0.0', + // hamburger + h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.menuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.props.dispatch(actions.toggleMenu()) }, - onClick() { state.dispatch(actions.showInfoPage()) } }), ]), ]) ) } -App.prototype.toggleMetamaskActive = function(){ - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } +App.prototype.renderDropdown = function() { + const props = this.props + return h(MenuDroppo, { + isOpen: props.menuOpen, + onClickOutside: (event) => { + this.props.dispatch(actions.closeMenu()) + }, + style: { + position: 'fixed', + right: 0, + zIndex: 0, + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu:() => this.props.dispatch(actions.closeMenu()), + action:() => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg', { ariaHidden: true }), + }), + + h(DropMenuItem, { + label: 'Lock Account', + closeMenu:() => this.props.dispatch(actions.closeMenu()), + action:() => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg', { ariaHidden: true }), + }), + + h(DropMenuItem, { + label: 'Help', + closeMenu:() => this.props.dispatch(actions.closeMenu()), + action:() => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg', { ariaHidden: true }), + }), + ]) } -App.prototype.renderPrimary = function(state){ - var state = this.props +App.prototype.renderPrimary = function(){ + var props = this.props - // If seed words haven't been dismissed yet, show them still. - /* - if (state.seedWords) { + if (props.seedWords) { return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'}) } - */ // show initialize screen - if (!state.isInitialized) { + if (!props.isInitialized) { // show current view - switch (state.currentView.name) { + switch (props.currentView.name) { case 'createVault': return h(CreateVaultScreen, {key: 'createVault'}) @@ -167,6 +237,9 @@ App.prototype.renderPrimary = function(state){ case 'restoreVault': return h(RestoreVaultScreen, {key: 'restoreVault'}) + case 'createVaultComplete': + return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'}) + default: return h(InitializeMenuScreen, {key: 'menuScreenInit'}) @@ -174,15 +247,12 @@ App.prototype.renderPrimary = function(state){ } // show unlock screen - if (!state.isUnlocked) { + if (!props.isUnlocked) { return h(UnlockScreen, {key: 'locked'}) } // show current view - switch (state.currentView.name) { - - case 'createVaultComplete': - return h(CreateVaultCompleteScreen, {key: 'created-vault'}) + switch (props.currentView.name) { case 'accounts': return h(AccountsScreen, {key: 'accounts'}) @@ -214,6 +284,18 @@ App.prototype.renderPrimary = function(state){ } } +App.prototype.toggleMetamaskActive = function(){ + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + App.prototype.hasPendingTxs = function() { var state = this.props var unconfTxs = state.unconfTxs diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index f79a533ba..eab9baf65 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') const actions = require('../actions') module.exports = ExportAccountView @@ -31,19 +32,28 @@ ExportAccountView.prototype.render = function() { and you should only do it if you know what you're doing.` var confirmation = `If you're absolutely sure, type "I understand" below and submit.` - return h('div', { key: 'exporting' }, [ - h('p.error', warning), - h('p', confirmation), - h('input#exportAccount', { - onKeyPress: this.onExportKeyPress.bind(this), - }), - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)) - }, 'Cancel'), - ]) + return ( + + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('p', confirmation), + h('input#exportAccount', { + onKeyPress: this.onExportKeyPress.bind(this), + }), + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)) + }, 'Cancel'), + ]) + + ) } if (accountExported) { diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js index c1450b516..6bae095d1 100644 --- a/ui/app/components/account-panel.js +++ b/ui/app/components/account-panel.js @@ -4,7 +4,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const addressSummary = require('../util').addressSummary const formatBalance = require('../util').formatBalance -const Identicon = require('identicon.js') +const Identicon = require('./identicon') const Panel = require('./panel') diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js new file mode 100644 index 000000000..c8e61278c --- /dev/null +++ b/ui/app/components/drop-menu-item.js @@ -0,0 +1,31 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + + +inherits(DropMenuItem, Component) +function DropMenuItem() { + Component.call(this) +} + +DropMenuItem.prototype.render = function() { + + return h('li.drop-menu-item', { + onClick:() => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Transat Medium', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + }, + }, [ + this.props.icon, + this.props.label, + ]) +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js new file mode 100644 index 000000000..20e24a9c7 --- /dev/null +++ b/ui/app/components/editable-label.js @@ -0,0 +1,52 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + + +inherits(EditableLabel, Component) +function EditableLabel() { + Component.call(this) +} + +EditableLabel.prototype.render = function() { + const props = this.props + let state = this.state + + if (state && state.isEditingLabel) { + + return h('div.editable-label', [ + h('input', { + defaultValue: props.textValue, + onKeyPress:(event) => { + this.saveIfEnter(event) + }, + }), + h('button', { + onClick:() => this.saveText(), + }, 'Save') + ]) + + } else { + return h('div', { + onClick:(event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function(event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function() { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + this.props.saveText(text) + this.setState({ isEditingLabel: false, textLabel: text }) +} diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js new file mode 100644 index 000000000..3f88ef2d4 --- /dev/null +++ b/ui/app/components/eth-balance.js @@ -0,0 +1,40 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const parseBalance = require('../util').parseBalance + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent() { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function() { + var state = this.props + var parsedAmount = parseBalance(state.value) + var beforeDecimal = parsedAmount[0] + var afterDecimal = parsedAmount[1] + var value = beforeDecimal+(afterDecimal ? '.'+afterDecimal : '') + var style = state.style + + return ( + + h('.ether-balance', { + style: style, + }, [ + h('.ether-balance-amount', { + style: { + display: 'inline', + }, + }, value), + h('.ether-balance-label', { + style: { + display: 'inline', + marginLeft: 6, + }, + }, 'ETH'), + ]) + + ) +} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js new file mode 100644 index 000000000..ef625cc62 --- /dev/null +++ b/ui/app/components/identicon.js @@ -0,0 +1,55 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const jazzicon = require('jazzicon') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent() { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function() { + var state = this.props + var diameter = state.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'inline-block', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function(){ + var state = this.props + var address = state.address + + if (!address) return + var numericRepresentation = jsNumberForAddress(address) + + var container = findDOMNode(this) + // jazzicon with hack to fix inline svg error + var diameter = state.diameter || this.defaultDiameter + var identicon = jazzicon(diameter, numericRepresentation) + var identiconSrc = identicon.innerHTML + var dataUri = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(identiconSrc) + var img = document.createElement('img') + img.src = dataUri + container.appendChild(img) +} + +function jsNumberForAddress(address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} diff --git a/ui/app/components/panel.js b/ui/app/components/panel.js index 25e6b7f0f..5d72d6068 100644 --- a/ui/app/components/panel.js +++ b/ui/app/components/panel.js @@ -2,7 +2,7 @@ const inherits = require('util').inherits const ethUtil = require('ethereumjs-util') const Component = require('react').Component const h = require('react-hyperscript') -const Identicon = require('identicon.js') +const Identicon = require('./identicon') module.exports = Panel @@ -18,26 +18,22 @@ Panel.prototype.render = function() { var identity = state.identity || {} var account = state.account || {} var isFauceting = state.isFauceting + var style = { + flex: '1 0 auto', + } - var identicon = new Identicon(state.identiconKey, 46).toString() - var identiconSrc = `data:image/png;base64,${identicon}` + if (state.onClick) style.cursor = 'pointer' return ( h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - }, + style, onClick: state.onClick, }, [ // account identicon h('.identicon-wrapper.flex-column.select-none', [ - h('img.identicon', { - src: identiconSrc, - style: { - border: 'none', - borderRadius: '20px', - } + h(Identicon, { + address: state.identiconKey, }), h('span.font-small', state.identiconLabel), ]), @@ -49,7 +45,7 @@ Panel.prototype.render = function() { return h('.flex-row.flex-space-between', { key: '' + Math.round(Math.random() * 1000000), }, [ - h('label.font-small', attr.key), + h('label.font-small.no-select', attr.key), h('span.font-small', attr.value), ]) }), diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3e153aecf..f85aab70f 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -1,55 +1,159 @@ const h = require('react-hyperscript') +const vreme = new (require('vreme')) const formatBalance = require('../util').formatBalance const addressSummary = require('../util').addressSummary const explorerLink = require('../../lib/explorer-link') const Panel = require('./panel') +const Identicon = require('./identicon') +const EtherBalance = require('./eth-balance') + module.exports = function(transactions, network) { - return h('section', [ + return ( - h('.current-domain-panel.flex-center.font-small', [ - h('span', 'Transactions'), - ]), + h('section.transaction-list', [ - h('.tx-list', { + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + }, + }, [ + 'Transactions', + ]), + + h('.tx-list', { style: { overflowY: 'auto', - height: '180px', + height: '204px', + padding: '0 20px', textAlign: 'center', }, - }, + }, ( - [ - - transactions.map((transaction) => { - console.dir(transaction) - - var panelOpts = { - key: `tx-${transaction.hash}`, - identiconKey: transaction.txParams.to, + transactions.length ? + transactions.map(renderTransaction) + : + [h('.flex-center', { style: { - cursor: 'pointer', + height: '100%', }, - onClick: (event) => { - var url = explorerLink(transaction.hash, parseInt(network)) - chrome.tabs.create({ url }); - }, - attributes: [ - { - key: 'TO', - value: addressSummary(transaction.txParams.to), - }, - { - key: 'VALUE', - value: formatBalance(transaction.txParams.value), - }, - ] - } + }, 'No transaction history...')] + + )) + + ]) + + ) + + + function renderTransaction(transaction, i){ + + var txParams = transaction.txParams + var date = formatDate(transaction.time) + + return ( + + h(`.transaction-list-item.flex-row.flex-space-between${transaction.hash ? '.pointer' : ''}`, { + key: `tx-${transaction.id + i}`, + onClick: (event) => { + if (!transaction.hash) return + var url = explorerLink(transaction.hash, parseInt(network)) + chrome.tabs.create({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + identicon(txParams, transaction), + ]), + + h('.flex-column', [ + + h('div', date), + + recipientField(txParams, transaction), + + ]), + + h(EtherBalance, { + value: txParams.value, + }), + ]) - return h(Panel, panelOpts) - }) - ] ) + } +} - ]) - } +function recipientField(txParams, transaction) { + if (txParams.to) { + return h('div', { + style: { + fontSize: 'small', + color: '#ABA9AA', + }, + }, [ + addressSummary(txParams.to), + failIfFailed(transaction), + ]) + + } else { + + return h('div', { + style: { + fontSize: 'small', + color: '#ABA9AA', + }, + },[ + 'Contract Published', + failIfFailed(transaction), + ]) + + } +} + +function formatDate(date){ + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function identicon(txParams, transaction) { + if (transaction.status === 'rejected') { + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + } + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + } + }) + } +} + +function failIfFailed(transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Failed)') + } +} diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 8ab79c3b9..9092c85c9 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -77,7 +77,8 @@ ConfirmTxScreen.prototype.render = function() { warningIfExists(state.warning), h(ReactCSSTransitionGroup, { - transitionName: "main", + className: 'css-transition-group', + transitionName: 'main', transitionEnterTimeout: 300, transitionLeaveTimeout: 300, }, [ diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css index dd1a755fb..b528cb9ab 100644 --- a/ui/app/css/fonts.css +++ b/ui/app/css/fonts.css @@ -1,2 +1,46 @@ @import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); \ No newline at end of file +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Transat Standard'; + src: url('/fonts/Transat Standard/transat_standard-webfont.eot'); + src: url('/fonts/Transat Standard/transat_standard-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/Transat Standard/transat_standard-webfont.woff') format('woff'), + url('/fonts/Transat Standard/transat_standard-webfont.ttf') format('truetype'), + url('/fonts/Transat Standard/transat_standard-webfont.svg#ywftsvg') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Transat Black'; + src: url('/fonts/Transat Black/transat_black-webfont.eot'); + src: url('/fonts/Transat Black/transat_black-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/Transat Black/transat_black-webfont.woff') format('woff'), + url('/fonts/Transat Black/transat_black-webfont.ttf') format('truetype'), + url('/fonts/Transat Black/transat_black-webfont.svg#ywftsvg') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Transat Medium'; + src: url('/fonts/Transat Medium/transat_medium-webfont.eot'); + src: url('/fonts/Transat Medium/transat_medium-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/Transat Medium/transat_medium-webfont.woff') format('woff'), + url('/fonts/Transat Medium/transat_medium-webfont.ttf') format('truetype'), + url('/fonts/Transat Medium/transat_medium-webfont.svg#ywftsvg') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Transat Light'; + src: url('/fonts/Transat Light/transat_light-webfont.eot'); + src: url('/fonts/Transat Light/transat_light-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/Transat Light/transat_light-webfont.woff') format('woff'), + url('/fonts/Transat Light/transat_light-webfont.ttf') format('truetype'), + url('/fonts/Transat Light/transat_light-webfont.svg#ywftsvg') format('svg'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 4871a650f..d6d1f91ac 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -14,11 +14,15 @@ application specific styles } html, body { - /*font-family: 'Open Sans', Arial, sans-serif;*/ - font-family: 'Roboto', 'Noto', sans-serif; + font-family: 'Transat Standard', Arial; color: #4D4D4D; font-weight: 300; line-height: 1.4em; + background: #F7F7F7; +} + +input:focus, textarea:focus { + outline: none; } #app-content { @@ -29,18 +33,18 @@ html, body { } button { + font-family: 'Transat Black'; outline: none; cursor: pointer; - margin: 10px; - padding: 6px; + /*margin: 10px;*/ + padding: 8px 12px; border: none; - border-radius: 3px; background: #F7861C; - font-weight: 500; color: white; transform-origin: center center; transition: transform 50ms ease-in; } + button:hover { transform: scale(1.1); } @@ -48,26 +52,6 @@ button:active { transform: scale(0.95); } -button.primary { - margin: 10px; - padding: 6px; - border: none; - border-radius: 3px; - background: #F7861C; - font-weight: 500; - color: white; -} - -input, textarea { - width: 300px; - padding: 6px; - border-radius: 6px; - border-style: solid; - outline: none; - border: 1px solid #F5A623; - background: #FAF6F0; -} - a { text-decoration: none; color: inherit; @@ -85,6 +69,16 @@ app color: #909090; } +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Transat Standard'; + text-transform: uppercase; +} + button.btn-thin { border: 1px solid; border-color: #4D4D4D; @@ -98,23 +92,25 @@ button.btn-thin { } .app-header { - padding-top: 20px; + padding: 6px 8px; } .app-header h1 { - font-size: 2em; - font-weight: 300; - height: 42px; + font-family: 'Transat Medium'; + text-transform: uppercase; + color: #AEAEAE; } h2.page-subtitle { + font-family: 'Transat Light'; + text-transform: uppercase; + color: #AEAEAE; font-size: 1em; - font-weight: 500; - height: 24px; - color: #F3C83E; + margin: 12px; } .app-primary { + } .app-footer { @@ -216,33 +212,70 @@ app sections margin: -2px 8px 0px -8px; } -.unlock-screen label { - color: #F3C83E; - font-weight: 500; +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; } .unlock-screen input[type=password] { - width: 60%; - height: 22px; - padding: 2px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ } -.unlock-screen input[type=password]:focus { - outline: none; - border: 3px solid #F3C83E; +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; } +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + + /* accounts */ .accounts-section { - margin: 0 20px; + margin: 0 0px; } -.current-domain-panel { - border: 1px solid #B7B7B7; +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; } .unconftx-link { @@ -289,8 +322,7 @@ app sections /* accounts screen */ .identity-section { - border: 2px solid #4D4D4D; - margin: 0; + } .identity-section .identity-panel { @@ -298,9 +330,6 @@ app sections border-bottom: 1px solid #B1B1B1; cursor: pointer; } -.identity-section .identity-panel:hover { - background: #F9F9F9; -} .identity-section .identity-panel.selected { background: white; @@ -311,10 +340,15 @@ app sections border-color: orange; } +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + /* account detail screen */ .account-detail-section { - margin: 0 20px; + } /* tx confirm */ @@ -333,157 +367,28 @@ app sections background: #FAF6F0; } +/* Send Screen */ -/* -react toggle -*/ +.send-screen { -/* overrides */ - -.react-toggle-track-check { - display: none; -} -.react-toggle-track-x { - display: none; } -/* modified original */ - -.react-toggle { - display: inline-block; - position: relative; - cursor: pointer; - background-color: transparent; - border: 0; - padding: 0; - - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - -webkit-tap-highlight-color: rgba(0,0,0,0); - -webkit-tap-highlight-color: transparent; +.send-screen section { + margin: 8px 16px; } -.react-toggle-screenreader-only { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; +.send-screen input { + width: 100%; + font-size: 12px; + letter-spacing: 0.1em; } -.react-toggle--disabled { - opacity: 0.5; - -webkit-transition: opacity 0.25s; - transition: opacity 0.25s; +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; } -.react-toggle-track { - width: 50px; - height: 24px; - padding: 0; - border-radius: 30px; - background-color: #4D4D4D; - -webkit-transition: all 0.2s ease; - -moz-transition: all 0.2s ease; - transition: all 0.2s ease; +.ether-balance-label { + color: #ABA9AA; } - -.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: #000000; -} - -.react-toggle--checked .react-toggle-track { - background-color: rgb(255, 174, 41); -} - -.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { - background-color: rgb(243, 151, 0); -} - -.react-toggle-track-check { - position: absolute; - width: 14px; - height: 10px; - top: 0px; - bottom: 0px; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - left: 8px; - opacity: 0; - -webkit-transition: opacity 0.25s ease; - -moz-transition: opacity 0.25s ease; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-check { - opacity: 1; - -webkit-transition: opacity 0.25s ease; - -moz-transition: opacity 0.25s ease; - transition: opacity 0.25s ease; -} - -.react-toggle-track-x { - position: absolute; - width: 10px; - height: 10px; - top: 0px; - bottom: 0px; - margin-top: auto; - margin-bottom: auto; - line-height: 0; - right: 10px; - opacity: 1; - -webkit-transition: opacity 0.25s ease; - -moz-transition: opacity 0.25s ease; - transition: opacity 0.25s ease; -} - -.react-toggle--checked .react-toggle-track-x { - opacity: 0; -} - -.react-toggle-thumb { - transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; - position: absolute; - top: 1px; - left: 1px; - width: 22px; - height: 22px; - border: 1px solid #4D4D4D; - border-radius: 50%; - background-color: #FAFAFA; - - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - - -webkit-transition: all 0.25s ease; - -moz-transition: all 0.25s ease; - transition: all 0.25s ease; -} - -.react-toggle--checked .react-toggle-thumb { - left: 27px; - border-color: #828282; -} -/* - .react-toggle--focus .react-toggle-thumb { - -webkit-box-shadow: 0px 0px 3px 2px #0099E0; - -moz-box-shadow: 0px 0px 3px 2px #0099E0; - box-shadow: 0px 0px 2px 3px #0099E0; - } - - .react-toggle:active .react-toggle-thumb { - -webkit-box-shadow: 0px 0px 5px 5px #0099E0; - -moz-box-shadow: 0px 0px 5px 5px #0099E0; - box-shadow: 0px 0px 5px 5px #0099E0; - } diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index b6b26402b..d9719b1e3 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -1,3 +1,13 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + /* lib */ .full-width { @@ -47,6 +57,10 @@ flex: none; } +.flex-basis-auto { + flex-basis: auto; +} + .flex-grow { flex: 1 1 auto; } @@ -86,13 +100,16 @@ } .select-none { - cursor: default; + cursor: inherit; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none; } +.pointer { + cursor: pointer; +} .cursor-pointer { cursor: pointer; transform-origin: center center; @@ -105,6 +122,10 @@ transform: scale(0.95); } +.cursor-disabled { + cursor: not-allowed; +} + .margin-bottom-sml { margin-bottom: 20px; } @@ -121,23 +142,27 @@ font-weight: bold; } +.text-transform-uppercase { + text-transform: uppercase; +} + .font-small { font-size: 12px; } -/* Send Screen */ -.send-screen { - margin: 0 20px; +.font-medium { + font-size: 1.2em; } -.send-screen section { - margin: 7px; - display: flex; - flex-direction: row; - justify-content: center; + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; } -.send-screen details { - width: 100%; -} -.send-screen section input { - width: 100%; + +.hover-white:hover { + background: white; } diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css index e2225a98d..393a944f9 100644 --- a/ui/app/css/transitions.css +++ b/ui/app/css/transitions.css @@ -1,48 +1,42 @@ -/* initial positions */ -.app-primary.from-right .main-enter { - transform: translateX(400px); +/* universal */ +.app-primary .main-enter { position: absolute; width: 100%; - transition: transform 300ms ease-in-out; -} -.app-primary.from-left .main-enter { - transform: translateX(-400px); - position: absolute; - width: 100%; - transition: transform 300ms ease-in-out; } /* center position */ -.app-primary .main-enter.main-enter-active, -.app-primary .main-leave { - transform: translateX(0px); - position: absolute; - width: 100%; - transition: transform 300ms ease-in-out; +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; } -/* final positions */ +/* exited positions */ .app-primary.from-left .main-leave-active { - transform: translateX(400px); - position: absolute; - width: 100%; - transition: transform 300ms ease-in-out; + transform: translateX(360px); + transition: transform 300ms ease-in; } .app-primary.from-right .main-leave-active { - transform: translateX(-400px); - position: absolute; - width: 100%; - transition: transform 300ms ease-in-out; + transform: translateX(-360px); + transition: transform 300ms ease-in; } /* loader transitions */ .loader-enter, .loader-leave-active { opacity: 0.0; - transition: opacity 150 ease-in-out; + transition: opacity 150 ease-in; } .loader-enter-active, .loader-leave { opacity: 1.0; - transition: opacity 150 ease-in-out; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); } diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js index 11b01a88b..2d54e7e19 100644 --- a/ui/app/first-time/init-menu.js +++ b/ui/app/first-time/init-menu.js @@ -29,15 +29,6 @@ InitializeMenuScreen.prototype.render = function() { switch (state.currentView.name) { - case 'createVault': - return h(CreateVaultScreen) - - case 'createVaultComplete': - return h(CreateVaultCompleteScreen) - - case 'restoreVault': - return this.renderRestoreVault() - default: return this.renderMenu() @@ -55,12 +46,12 @@ InitializeMenuScreen.prototype.renderMenu = function() { h('.initialize-screen.flex-column.flex-center.flex-grow', [ - h('h2.page-subtitle', 'Welcome!'), - h(Mascot, { animationEventEmitter: this.animationEventEmitter, }), + h('h2.page-subtitle', 'MetaMask'), + h('button.btn-thin', { onClick: this.showCreateVault.bind(this), }, 'Create New Vault'), @@ -80,31 +71,6 @@ InitializeMenuScreen.prototype.renderMenu = function() { ) } -InitializeMenuScreen.prototype.renderRestoreVault = function() { - var state = this.props - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.showInitializeMenu.bind(this), - }), - h('h2.page-subtitle', 'Restore Vault'), - ]), - - - h('h3', 'Coming soon....'), - // h('textarea.twelve-word-phrase', { - // value: 'hey ho what the actual hello rubber duck bumbersnatch crumplezone frankenfurter', - // }), - - ]) - - ) -} - // InitializeMenuScreen.prototype.splitWor = function() { // this.props.dispatch(actions.showInitializeMenu()) // } diff --git a/ui/app/loading.js b/ui/app/loading.js index 9288256de..f6279d5cf 100644 --- a/ui/app/loading.js +++ b/ui/app/loading.js @@ -23,7 +23,8 @@ LoadingIndicator.prototype.render = function() { return ( h(ReactCSSTransitionGroup, { - transitionName: "loader", + className: 'css-transition-group', + transitionName: 'loader', transitionEnterTimeout: 150, transitionLeaveTimeout: 150, }, [ diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 309351956..a29a8f79c 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -22,6 +22,7 @@ function reduceApp(state, action) { var seedWords = state.metamask.seedWords var appState = extend({ + menuOpen: false, currentView: seedWords ? seedConfView : defaultView, accountDetail: { subview: 'transactions', @@ -34,6 +35,16 @@ function reduceApp(state, action) { switch (action.type) { + case actions.TOGGLE_MENU: + return extend(appState, { + menuOpen: !appState.menuOpen, + }) + + case actions.SET_MENU_STATE: + return extend(appState, { + menuOpen: action.value, + }) + // intialize case actions.SHOW_CREATE_VAULT: @@ -154,7 +165,7 @@ function reduceApp(state, action) { accountExport: 'none', privateKey: '', }, - transForward: true, + transForward: false, }) case actions.BACK_TO_ACCOUNT_DETAIL: @@ -177,9 +188,15 @@ function reduceApp(state, action) { currentView: { name: seedWords ? 'createVaultComplete' : 'accounts', }, - transForward: appState.currentView.name == 'locked', + transForward: true, isLoading: false, warning: null, + scrollToBottom: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, }) case actions.SHOW_CONF_TX_PAGE: @@ -278,10 +295,13 @@ function reduceApp(state, action) { case actions.CLEAR_SEED_WORD_CACHE: return extend(appState, { transForward: true, - currentView: { - name: 'accounts', - }, + currentView: {}, isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, }) case actions.DISPLAY_WARNING: diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 9398f1497..a45327189 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -29,6 +29,7 @@ function reduceMetamask(state, action) { return extend(metamaskState, { isUnlocked: true, isInitialized: true, + selectedAccount: action.value, }) case actions.LOCK_METAMASK: @@ -69,18 +70,38 @@ function reduceMetamask(state, action) { } return newState + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: var newState = extend(metamaskState, { + isUnlocked: true, isInitialized: true, + selectedAccount: action.value, }) delete newState.seedWords return newState - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(metamaskState, { + case actions.SHOW_ACCOUNT_DETAIL: + const newState = extend(metamaskState, { isUnlocked: true, isInitialized: true, + selectedAccount: action.value, + selectedAddress: action.value, }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) default: return metamaskState diff --git a/ui/app/send.js b/ui/app/send.js index ff8ef4d65..ba4e5bfff 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -2,10 +2,13 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect +const Identicon = require('./components/identicon') const actions = require('./actions') const util = require('./util') const numericBalance = require('./util').numericBalance -const AccountPanel = require('./components/account-panel') +const formatBalance = require('./util').formatBalance +const addressSummary = require('./util').addressSummary +const EtherBalance = require('./components/eth-balance') const ethUtil = require('ethereumjs-util') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -18,6 +21,8 @@ function mapStateToProps(state) { warning: state.appState.warning, } + result.error = result.warning && result.warning.split('.')[0] + result.account = result.accounts[result.address] result.identity = result.identities[result.address] result.balance = result.account ? numericBalance(result.account.balance) : null @@ -32,95 +37,190 @@ function SendTransactionScreen() { SendTransactionScreen.prototype.render = function() { var state = this.props + var address = state.address var account = state.account var identity = state.identity return ( + h('.send-screen.flex-column.flex-grow', [ - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.back.bind(this), - }), - h('h2.page-subtitle', 'Send Transaction'), + // + // Sender Profile + // + + h('.account-data-subsection.flex-column.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: 28, + }, + }, [ + + // invisible placeholder for later + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // small accounts nav + h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', { + onClick: this.navigateToAccounts.bind(this), + }), + + ]), + + // account label + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: 8, + marginBottom: 8, + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: 8, + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + // h('div', formatBalance(account && account.balance)), + h(EtherBalance, { + value: account && account.balance, + }) + + ]), + ]), - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - }), + // + // Required Fields + // - h('section.recipient', [ - h('input.address', { + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 32, + marginBottom: 16, + }, + }, [ + 'Send Transaction', + ]), + + // error message + state.error && h('span.error.flex-center', state.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h('input.large-input', { + name: 'address', placeholder: 'Recipient Address', }) ]), - h('section.ammount', [ - h('input.ether', { + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', placeholder: 'Amount', type: 'number', - style: { marginRight: '6px' } + style: { + marginRight: 6, + }, }), - h('select.currency', { - name: 'currency', - }, [ - h('option', { value: 'ether' }, 'Ether (1e18 wei)'), - h('option', { value: 'wei' }, 'Wei'), - ]), - ]), - h('section.data', [ - h('details', [ - h('summary', { - style: {cursor: 'pointer'}, - }, 'Advanced'), - h('textarea.txData', { - type: 'textarea', - placeholder: 'Transaction data (optional)', - style: { - height: '100px', - width: '100%', - resize: 'none', - } - }) - ]) - ]), - - h('section', { - }, [ - h('button', { + h('button.primary', { onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, }, 'Send') + + ]), + + // + // Optional Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 16, + marginBottom: 16, + }, + }, [ + 'Tranasactional Data (optional)', + ]), + + // 'data' field + h('section.flex-row.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + } + }), ]), - state.warning ? h('span.error', state.warning.split('.')[0]) : null, ]) + ) } +SendTransactionScreen.prototype.navigateToAccounts = function(event){ + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + SendTransactionScreen.prototype.back = function() { var address = this.props.address this.props.dispatch(actions.backToAccountDetail(address)) } -SendTransactionScreen.prototype.onSubmit = function(event) { - var recipient = document.querySelector('input.address').value +SendTransactionScreen.prototype.onSubmit = function() { - var inputAmount = parseFloat(document.querySelector('input.ether').value) - var currency = document.querySelector('select.currency').value - var value = util.normalizeNumberToWei(inputAmount, currency) - - var balance = this.props.balance + const recipient = document.querySelector('input[name="address"]').value + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance if (value.gt(balance)) { var message = 'Insufficient funds.' return this.props.dispatch(actions.displayWarning(message)) } - if (recipient.length !== 42) { - var message = 'Recipient address is the incorrect length.' + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + var message = 'Recipient address is invalid.' return this.props.dispatch(actions.displayWarning(message)) } @@ -128,12 +228,11 @@ SendTransactionScreen.prototype.onSubmit = function(event) { this.props.dispatch(actions.showLoadingIndication()) var txParams = { - to: recipient, from: this.props.address, value: '0x' + value.toString(16), } - var txData = document.querySelector('textarea.txData').value + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) if (txData) txParams.data = txData this.props.dispatch(actions.signTx(txParams)) diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 8aac1b1ff..687bb5e52 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -29,19 +29,25 @@ UnlockScreen.prototype.render = function() { h('.unlock-screen.flex-column.flex-center.flex-grow', [ - h('h2.page-subtitle', 'Welcome!'), - h(Mascot, { animationEventEmitter: this.animationEventEmitter, }), - h('label', { - htmlFor: 'password-box', - }, 'Enter Password:'), + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), - h('input', { + h('input.large-input', { type: 'password', id: 'password-box', + placeholder: 'enter password', + style: { + + }, onKeyPress: this.onKeyPress.bind(this), onInput: this.inputChanged.bind(this), }), @@ -54,6 +60,9 @@ UnlockScreen.prototype.render = function() { h('button.primary.cursor-pointer', { onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, }, 'Unlock'), ]) diff --git a/ui/app/util.js b/ui/app/util.js index 5dbcffa7e..81a029350 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -21,13 +21,17 @@ for (var currency in valueTable) { module.exports = { valuesFor: valuesFor, addressSummary: addressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, numericBalance: numericBalance, + parseBalance: parseBalance, formatBalance: formatBalance, dataSize: dataSize, readableDate: readableDate, ethToWei: ethToWei, weiToEth: weiToEth, normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, normalizeNumberToWei: normalizeNumberToWei, valueTable: valueTable, bnTable: bnTable, @@ -41,7 +45,21 @@ function valuesFor(obj) { } function addressSummary(address) { - return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...' + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0,2+8)+'...'+checked.slice(-4) : '...' +} + +function isValidAddress(address) { + var prefixed = ethUtil.addHexPrefix(address) + return isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase(address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper } // Takes wei Hex, returns wei BN, even if input is null @@ -65,16 +83,30 @@ function weiToEth(bn) { return eth } -var decimalsToKeep = 4 -function formatBalance(balance) { - if (!balance || balance === '0x0') return 'None' +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance(balance, decimalsToKeep) { + if (decimalsToKeep === undefined) decimalsToKeep = 4 + if (!balance || balance === '0x0') return ['0', ''] var wei = numericBalance(balance) var padded = wei.toString(10) var len = padded.length - var nonZeroIndex = padded.match(/[^0]/) && padded.match(/[^0]/).index + var match = padded.match(/[^0]/) + var nonZeroIndex = match && match.index var beforeDecimal = padded.substr(nonZeroIndex ? nonZeroIndex : 0, len - 18) || '0' var afterDecimal = padded.substr(len - 18, decimalsToKeep) - return `${beforeDecimal}.${afterDecimal} ETH` + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns "None" or "${formattedAmount} ETH" +function formatBalance(balance) { + var parsed = parseBalance(balance) + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + if (beforeDecimal === '0' && afterDecimal === '') return 'None' + var result = beforeDecimal + if (afterDecimal) result += '.'+afterDecimal + result += ' ETH' + return result } function dataSize(data) { @@ -91,9 +123,23 @@ function normalizeToWei(amount, currency) { return amount } -var multiple = new ethUtil.BN('1000', 10) +function normalizeEthStringToWei(str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while(decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) function normalizeNumberToWei(n, currency) { - var enlarged = n * 1000 + var enlarged = n * 10000 var amount = new ethUtil.BN(String(enlarged), 10) return normalizeToWei(amount, currency).div(multiple) } diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg new file mode 100644 index 000000000..8a06be6b9 Binary files /dev/null and b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/design/02b-metamask-AccDetails-Send.jpg new file mode 100644 index 000000000..10f2d27fd Binary files /dev/null and b/ui/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/ui/design/05-metamask-Menu.jpg b/ui/design/05-metamask-Menu.jpg new file mode 100644 index 000000000..0a43d7b2a Binary files /dev/null and b/ui/design/05-metamask-Menu.jpg differ