diff --git a/.eslintignore b/.eslintignore index 089ce7a5..94a7b116 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ build coverage node_modules src/flowtype/npm +scripts/solidity/.* **/_old/* \ No newline at end of file diff --git a/.flowconfig b/.flowconfig index e42f6028..a3a80352 100644 --- a/.flowconfig +++ b/.flowconfig @@ -9,7 +9,7 @@ .*/node_modules/react-router-redux/.* .*/node_modules/oboe/test/.* .*/_old/.* -.*/public/solidity/.* +.*/scripts/solidity/.* .*/build/.* [libs] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5c0f6536..2ec28061 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,20 +1,109 @@ image: node:8 -before_script: - - yarn install - cache: paths: - - node_modules/ + - node_modules + +stages: + - test + - build + - deploy + +lint: + stage: test + script: + - yarn install + - yarn run lint flow: + stage: test script: - yarn run flow -test:lint: - script: - - yarn run lint - -test:unit: +unit: + stage: test script: + - yarn install - yarn run test + +build development: + stage: build + script: + - yarn install + - yarn run build:dev + artifacts: + expire_in: 1 week + paths: + - build/dev + +build beta: + stage: build + script: + - yarn install + - yarn run build:beta + artifacts: + expire_in: 1 week + paths: + - build/beta + - scripts/s3sync.sh + +build stable: + stage: build + script: + - yarn install + - yarn run build:stable + artifacts: + expire_in: 1 week + paths: + - build/stable + - scripts/s3sync.sh + +deploy review: + stage: deploy + variables: + GIT_STRATEGY: none + dependencies: + - build development + script: + - echo "Deploy a review app" + - '[ -z "${DEPLOY_BASE_DIR}" ] && echo "Deploy base dir cannot be empty" && exit 255' + - env + - mkdir -p "${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}" + - echo "Copy dev build to web server ${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}..." + - rsync --delete -va build/dev/ "${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}/" + only: + - branches + tags: + - deploy + +deploy stage beta: + stage: deploy + variables: + GIT_STRATEGY: none + AWS_ACCESS_KEY_ID: $STAGE_AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $STAGE_AWS_SECRET_ACCESS_KEY + when: manual + dependencies: + - build beta + script: + - scripts/s3sync.sh stage beta + only: + - beta + tags: + - deploy + +deploy stage stable: + stage: deploy + variables: + GIT_STRATEGY: none + AWS_ACCESS_KEY_ID: $STAGE_AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $STAGE_AWS_SECRET_ACCESS_KEY + when: manual + dependencies: + - build stable + script: + - scripts/s3sync.sh stage stable + only: + - stable + tags: + - deploy diff --git a/Dockerfile b/Dockerfile index 48abcd1d..7d2db3a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:8 +FROM node:9.3 + +ARG BUILD_TYPE=stable WORKDIR /trezor-wallet-app @@ -9,7 +11,7 @@ RUN yarn install COPY . /trezor-wallet-app -RUN yarn run build:prod +RUN yarn run build:${BUILD_TYPE} EXPOSE 8080 CMD [ "yarn", "run", "prod-server" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e40ce5de --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Run docker build +# usage: +# make build-beta +# make build-stable +build-%: + ./scripts/docker-build.sh $* + +# s3sync with stage.mytrezor.com +# Upload requested build for testing purposes +# usage: +# make stage-beta +# make stage-stable +stage-%: + ./scripts/s3sync.sh stage $* + +# s3sync with beta.mytrezor.com +# Upload build/beta only +# usage: +# make beta +beta: + ./scripts/s3sync.sh beta beta + +# s3sync with wallet.mytrezor.com +# Upload build/stable only +# usage: +# make stable +stable: + ./scripts/s3sync.sh stable stable \ No newline at end of file diff --git a/README.md b/README.md index 5e196b09..0b2f694d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Trezor Ethereum Wallet +# Trezor Wallet To install dependencies run `npm install` or `yarn` To start locally run `npm run dev` or `yarn run dev` diff --git a/package.json b/package.json index 7bfc3947..48dec77d 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,12 @@ "bin": { "flow": "./node_modules/flow-bin" }, - "license": "T-RSL", + "license": "SEE LICENSE IN LICENSE.md", "scripts": { "dev": "npx webpack-dev-server --config webpack/dev.babel.js", "dev:local": "npx webpack-dev-server --config webpack/local.babel.js", "build": "rimraf build && run-s build:*", - "build:prod": "rimraf build/prod && npx webpack --config webpack/production.babel.js --output-path build/prod --progress --bail", + "build:stable": "rimraf build/stable && npx webpack --config webpack/production.babel.js --output-path build/stable --progress --bail", "build:beta": "rimraf build/beta && cross-env BUILD=beta npx webpack --config webpack/production.babel.js --output-path build/beta --progress --bail", "build:dev": "rimraf build.dev && cross-env BUILD=development npx webpack --config webpack/production.babel.js --output-path build/dev --progress --bail", "flow": "flow check src", @@ -21,13 +21,15 @@ "test": "run-s test:*", "test:unit": "npx jest", "test-unit:watch": "npx jest -o --watch", - "prod-server": "node ./server/index.js" + "server:beta": "node ./server/index.js --buildType=beta", + "server:stable": "node ./server/index.js --buildType=stable" }, "dependencies": { "babel": "^6.23.0", "babel-core": "^6.26.3", "bignumber.js": "2.4.0", "color-hash": "^1.0.3", + "commander": "^2.19.0", "copy-webpack-plugin": "^4.5.2", "cross-env": "^5.2.0", "date-fns": "^1.29.0", @@ -67,7 +69,7 @@ "styled-components": "^3.4.9", "styled-media-query": "^2.0.2", "styled-normalize": "^8.0.0", - "trezor-connect": "^5.0.32", + "trezor-connect": "6.0.0", "web3": "1.0.0-beta.35", "webpack": "^4.16.3", "webpack-build-notifier": "^0.1.29", diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index b4bceccd..3d94f9d3 100755 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -1,4 +1,19 @@ #!/bin/bash cd "$(dirname "$0")" -docker build -t trezor-wallet ../ \ No newline at end of file + +if [[ "$1" == "dev" || "$1" == "beta" || "$1" == "stable" ]] + then + mkdir -p ../build + rm -rf ../build/$1 + docker ps -q --filter "name=trezor-wallet" | grep -q . && docker stop trezor-wallet && docker rm -fv trezor-wallet + docker build -t trezor-wallet ../ --build-arg BUILD_TYPE=$1 + docker run -p 8080:8080 -d --name trezor-wallet trezor-wallet:latest + docker cp trezor-wallet:/trezor-wallet-app/build/$1 ../build/$1 + docker stop trezor-wallet + docker rm trezor-wallet + echo "DONE!" + echo "Build directory: build/"$1 + else + echo "invalid parameters... valid parameters are (dev, beta, stable)" +fi \ No newline at end of file diff --git a/scripts/s3sync.sh b/scripts/s3sync.sh new file mode 100755 index 00000000..41fc5e3f --- /dev/null +++ b/scripts/s3sync.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# Before first use: +# Install awscli (pip install awscli) +# Configure access credentials (aws configure), region is "eu-central-1" + +# Usage: +# ./s3sync.sh DESTINATION SOURCE [clear] +# @DESTINATION: required, destination server +# @SOURCE: required, build +# @CLEAR: optional, delete previous uploads +# ./s3sync.sh stage beta +# ./s3sync.sh stage stable +# ./s3sync.sh stage stable clear +# ./s3sync.sh beta beta +# ./s3sync.sh stable stable + +function confirm { + read -r -p "Are you sure? [y/N] " response + if [[ $response =~ ^(yes|y)$ ]]; then + echo "let's go!" + else + exit 2 + fi +} + + +# Validate params +if [ "x$1" == "x" ] || [ "x$2" == "x" ]; then + echo "Invalid params" + echo "./s3sync.sh stage|beta|stable beta|stable [clear]" + exit 1 +fi + +# Validate destination param +if [ "x$1" != "xstage" ] && [ "x$1" != "xbeta" ] && [ "x$1" != "xstable" ]; then + echo "Invalid destination: "$1 + echo "use: stage|beta|stable" + exit 1 +fi + +# Validate source param +if [ "x$2" != "xbeta" ] && [ "x$2" != "xstable" ]; then + echo "Invalid source: "$2 + echo "use: beta|stable" + exit 1 +fi + +# Set source directory +if [ "x$2" == "xbeta" ]; then + SOURCE=../build/beta + +elif [ "x$2" == "xstable" ]; then + SOURCE=../build/stable +fi + +# Set destination +if [ "x$1" == "xstage" ]; then + BUCKET=stage.mytrezor.com + DISTRIBUTION_ID="E24M0QWO692FQL" +elif [ "x$1" == "xbeta" ]; then + BUCKET=beta.mytrezor.com + DISTRIBUTION_ID="E1PONNHWUNCQ9M" +elif [ "x$2" == "xstable" ]; then + BUCKET=wallet.mytrezor.com + DISTRIBUTION_ID="EZM01GFTITGVD" +fi + +echo "sync "$SOURCE" with "$BUCKET"/next" + +if [ "x$1" == "xbeta" ] || [ "x$1" == "xstable" ]; then + confirm +fi + +set -e +cd `dirname $0` + +if [ "x$3" == "x-clear" ]; then + aws s3 sync --delete --cache-control 'public, max-age=3600' $SOURCE s3://$BUCKET/next +else + aws s3 sync --cache-control 'public, max-age=3600' $SOURCE s3://$BUCKET/next +fi + +aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/next/*' + +echo "DONE" \ No newline at end of file diff --git a/public/solidity/erc20.json b/scripts/solidity/erc20.json similarity index 100% rename from public/solidity/erc20.json rename to scripts/solidity/erc20.json diff --git a/public/solidity/grzegorz-token.js b/scripts/solidity/grzegorz-token.js similarity index 100% rename from public/solidity/grzegorz-token.js rename to scripts/solidity/grzegorz-token.js diff --git a/public/solidity/lahodka-token.js b/scripts/solidity/lahodka-token.js similarity index 100% rename from public/solidity/lahodka-token.js rename to scripts/solidity/lahodka-token.js diff --git a/public/solidity/rinkeby-token.js b/scripts/solidity/rinkeby-token.js similarity index 100% rename from public/solidity/rinkeby-token.js rename to scripts/solidity/rinkeby-token.js diff --git a/public/solidity/test-token.js b/scripts/solidity/test-token.js similarity index 100% rename from public/solidity/test-token.js rename to scripts/solidity/test-token.js diff --git a/server/index.js b/server/index.js index e179367b..7bdd18cd 100644 --- a/server/index.js +++ b/server/index.js @@ -3,6 +3,12 @@ const morgan = require('morgan'); const fs = require('fs'); const path = require('path'); const https = require('https'); +const commander = require('commander'); + +commander + .version('0.1.0') + .option('-b, --buildType [type]', 'Set the build type') + .parse(process.argv); const privateKey = fs.readFileSync('server/key.pem').toString(); const certificate = fs.readFileSync('server/cert.pem').toString(); @@ -16,11 +22,11 @@ const PORT = process.env.PORT || 8080; app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms')); // Serve static assets -app.use(express.static(path.resolve(__dirname, '..', 'build'))); +app.use(express.static(path.resolve(__dirname, '..', 'build', commander.buildType))); // Always return the main index.html, so react-router render the route in the client app.get('*', (req, res) => { - res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html')); + res.sendFile(path.resolve(__dirname, '..', 'build', commander.buildType, 'index.html')); }); const httpsServer = https.createServer(credentials, app); diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index 49fbf0fa..786da55c 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -10,6 +10,7 @@ import * as PENDING from 'actions/constants/pendingTx'; import * as WALLET from 'actions/constants/wallet'; import { httpRequest } from 'utils/networkUtils'; import * as buildUtils from 'utils/build'; +import * as storageUtils from 'utils/storage'; import { findAccountTokens } from 'reducers/TokensReducer'; import type { Account } from 'reducers/AccountsReducer'; @@ -17,7 +18,6 @@ import type { Token } from 'reducers/TokensReducer'; import type { PendingTx } from 'reducers/PendingTxReducer'; import type { Discovery } from 'reducers/DiscoveryReducer'; - import type { TrezorDevice, ThunkAction, @@ -43,22 +43,15 @@ export type StorageAction = { error: string, }; -const get = (key: string): ?string => { - try { - return window.localStorage.getItem(key); - } catch (error) { - // available = false; - return null; - } -}; - -const set = (key: string, value: string | boolean): void => { - try { - window.localStorage.setItem(key, value); - } catch (error) { - console.error(`Local Storage ERROR: ${error}`); - } -}; +const TYPE: 'local' = 'local'; +const { STORAGE_PATH } = storageUtils; +const KEY_VERSION: string = `${STORAGE_PATH}version`; +const KEY_DEVICES: string = `${STORAGE_PATH}devices`; +const KEY_ACCOUNTS: string = `${STORAGE_PATH}accounts`; +const KEY_DISCOVERY: string = `${STORAGE_PATH}discovery`; +const KEY_TOKENS: string = `${STORAGE_PATH}tokens`; +const KEY_PENDING: string = `${STORAGE_PATH}pending`; +const KEY_BETA_MODAL: string = '/betaModalPrivacy'; // this key needs to be compatible with "parent" (old) wallet // https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js // or @@ -80,25 +73,25 @@ export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): const discovery: Array = findDiscovery(devices, getState().discovery); // save devices - set('devices', JSON.stringify(devices)); + storageUtils.set(TYPE, KEY_DEVICES, JSON.stringify(devices)); // save already preloaded accounts - set('accounts', JSON.stringify(accounts)); + storageUtils.set(TYPE, KEY_ACCOUNTS, JSON.stringify(accounts)); // save discovery state - set('discovery', JSON.stringify(discovery)); + storageUtils.set(TYPE, KEY_DISCOVERY, JSON.stringify(discovery)); // tokens - set('tokens', JSON.stringify(tokens)); + storageUtils.set(TYPE, KEY_TOKENS, JSON.stringify(tokens)); // pending transactions - set('pending', JSON.stringify(pending)); + storageUtils.set(TYPE, KEY_PENDING, JSON.stringify(pending)); }; export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch): void => { if (!event.newValue) return; - if (event.key === 'devices') { + if (event.key === KEY_DEVICES) { // check if device was added/ removed // const newDevices: Array = JSON.parse(event.newValue); // const myDevices: Array = getState().connect.devices.filter(d => d.features); @@ -113,28 +106,28 @@ export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch) // const diff = oldDevices.filter(d => newDevices.indexOf()) } - if (event.key === 'accounts') { + if (event.key === KEY_ACCOUNTS) { dispatch({ type: ACCOUNT.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'tokens') { + if (event.key === KEY_TOKENS) { dispatch({ type: TOKEN.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'pending') { + if (event.key === KEY_PENDING) { dispatch({ type: PENDING.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'discovery') { + if (event.key === KEY_DISCOVERY) { dispatch({ type: DISCOVERY.FROM_STORAGE, payload: JSON.parse(event.newValue), @@ -142,14 +135,13 @@ export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch) } }; -const VERSION: string = '1'; - const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => { if (typeof window.localStorage === 'undefined') return; try { const config: Config = await httpRequest(AppConfigJSON, 'json'); + // remove ropsten testnet from config networks if (!buildUtils.isDev()) { const index = config.networks.findIndex(c => c.shortcut === 'trop'); delete config.networks[index]; @@ -161,18 +153,6 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => dispatch(update(event)); }); - // validate version - const version: ?string = get('version'); - if (version !== VERSION) { - try { - window.localStorage.clear(); - window.sessionStorage.clear(); - } catch (error) { - // empty - } - set('version', VERSION); - } - // load tokens const tokens = await config.networks.reduce(async (promise: Promise, network: Network): Promise => { const collection: TokensCollection = await promise; @@ -195,9 +175,17 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => } }; +const VERSION: string = '1'; const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { - const devices: ?string = get('devices'); + // validate version + const version: ?string = storageUtils.get(TYPE, KEY_VERSION); + if (version && version !== VERSION) { + storageUtils.clearAll(); + } + storageUtils.set(TYPE, KEY_VERSION, VERSION); + + const devices: ?string = storageUtils.get(TYPE, KEY_DEVICES); if (devices) { dispatch({ type: CONNECT.DEVICE_FROM_STORAGE, @@ -205,7 +193,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const accounts: ?string = get('accounts'); + const accounts: ?string = storageUtils.get(TYPE, KEY_ACCOUNTS); if (accounts) { dispatch({ type: ACCOUNT.FROM_STORAGE, @@ -213,7 +201,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const userTokens: ?string = get('tokens'); + const userTokens: ?string = storageUtils.get(TYPE, KEY_TOKENS); if (userTokens) { dispatch({ type: TOKEN.FROM_STORAGE, @@ -221,7 +209,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const pending: ?string = get('pending'); + const pending: ?string = storageUtils.get(TYPE, KEY_PENDING); if (pending) { dispatch({ type: PENDING.FROM_STORAGE, @@ -229,7 +217,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const discovery: ?string = get('discovery'); + const discovery: ?string = storageUtils.get(TYPE, KEY_DISCOVERY); if (discovery) { dispatch({ type: DISCOVERY.FROM_STORAGE, @@ -238,7 +226,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { } if (buildUtils.isDev() || buildUtils.isBeta()) { - const betaModal = get('/betaModalPrivacy'); + const betaModal = Object.keys(window.localStorage).find(key => key.indexOf(KEY_BETA_MODAL) >= 0); if (!betaModal) { dispatch({ type: WALLET.SHOW_BETA_DISCLAIMER, @@ -258,6 +246,6 @@ export const loadData = (): ThunkAction => (dispatch: Dispatch, getState: GetSta }; export const hideBetaDisclaimer = (): ThunkAction => (dispatch: Dispatch): void => { - set('/betaModalPrivacy', true); + storageUtils.set(TYPE, KEY_BETA_MODAL, true); dispatch(loadJSON()); }; diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index d4c5842b..18eae118 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -2,6 +2,7 @@ import { push, LOCATION_CHANGE } from 'react-router-redux'; import { CONTEXT_NONE } from 'actions/constants/modal'; +import { SET_INITIAL_URL } from 'actions/constants/wallet'; import { routes } from 'support/routes'; import * as deviceUtils from 'utils/device'; @@ -57,7 +58,7 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10)); } else { - device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device)); + device = devices.find(d => ((!d.features || d.mode === 'bootloader') && d.path === params.device) || (d.features && d.features.device_id === params.device)); } if (!device) return false; @@ -247,6 +248,8 @@ const sortDevices = (devices: Array): Array => devic * Redirect to requested device */ export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + if (dispatch(setInitialUrl())) return; + const url: ?string = dispatch(getDeviceUrl(device)); if (!url) return; @@ -352,7 +355,12 @@ export const setInitialUrl = (): PayloadAction => (dispatch: Dispatch, state: {}, }, })); + if (valid === initialPathname) { + // reset initial url + dispatch({ + type: SET_INITIAL_URL, + }); dispatch(goto(valid)); return true; } diff --git a/src/actions/SessionStorageActions.js b/src/actions/SessionStorageActions.js index 33c953c8..e713a791 100644 --- a/src/actions/SessionStorageActions.js +++ b/src/actions/SessionStorageActions.js @@ -1,5 +1,5 @@ /* @flow */ - +import * as storageUtils from 'utils/storage'; import type { State as SendFormState } from 'reducers/SendFormReducer'; import type { @@ -9,52 +9,39 @@ import type { Dispatch, } from 'flowtype'; -const TX_PREFIX: string = 'trezor:draft-tx:'; +const TYPE: 'session' = 'session'; +const { STORAGE_PATH } = storageUtils; +const KEY_TX_DRAFT: string = `${STORAGE_PATH}txdraft`; + +const getTxDraftKey = (getState: GetState): string => { + const { pathname } = getState().router.location; + return `${KEY_TX_DRAFT}${pathname}`; +}; export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - if (typeof window.localStorage === 'undefined') return; - const state = getState().sendForm; if (state.untouched) return; - const location = getState().router.location.pathname; - try { - // save state as it is - // "loadDraftTransaction" will do the validation - window.sessionStorage.setItem(`${TX_PREFIX}${location}`, JSON.stringify(state)); - } catch (error) { - console.error(`Saving sessionStorage error: ${error}`); - } + const key = getTxDraftKey(getState); + storageUtils.set(TYPE, key, JSON.stringify(state)); }; export const loadDraftTransaction = (): PayloadAction => (dispatch: Dispatch, getState: GetState): ?SendFormState => { - if (typeof window.localStorage === 'undefined') return null; - - try { - const location = getState().router.location.pathname; - const value: string = window.sessionStorage.getItem(`${TX_PREFIX}${location}`); - const state: ?SendFormState = JSON.parse(value); - if (state) { - // decide if draft is valid and should be returned - // ignore this draft if has any error - if (Object.keys(state.errors).length > 0) { - window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); - return null; - } - return state; - } - } catch (error) { - console.error(`Loading sessionStorage error: ${error}`); + const key = getTxDraftKey(getState); + const value: ?string = storageUtils.get(TYPE, key); + if (!value) return null; + const state: ?SendFormState = JSON.parse(value); + if (!state) return null; + // decide if draft is valid and should be returned + // ignore this draft if has any error + if (Object.keys(state.errors).length > 0) { + storageUtils.remove(TYPE, key); + return null; } - return null; + return state; }; export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - if (typeof window.localStorage === 'undefined') return; - const location = getState().router.location.pathname; - try { - window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); - } catch (error) { - console.error(`Clearing sessionStorage error: ${error}`); - } -}; \ No newline at end of file + const key = getTxDraftKey(getState); + storageUtils.remove(TYPE, key); +}; diff --git a/src/components/Notification/index.js b/src/components/Notification/index.js index 8b4e5bb2..fdff20e7 100644 --- a/src/components/Notification/index.js +++ b/src/components/Notification/index.js @@ -87,7 +87,7 @@ const StyledIcon = styled(Icon)` `; const IconWrapper = styled.div` - min-width: 20px; + min-width: 30px; `; const Texts = styled.div` diff --git a/src/config/variables.js b/src/config/variables.js index af2beb2e..125a5604 100644 --- a/src/config/variables.js +++ b/src/config/variables.js @@ -3,6 +3,7 @@ export const FONT_SIZE = { SMALLER: '12px', SMALL: '14px', BASE: '16px', + WALLET_TITLE: '18px', TOP_MENU: '17px', WALLET_TITLE: '18px', BIG: '21px', diff --git a/src/constants/coins.js b/src/constants/coins.js index ac70469c..4014242c 100644 --- a/src/constants/coins.js +++ b/src/constants/coins.js @@ -2,47 +2,47 @@ export default [ { id: 'btc', coinName: 'Bitcoin', - url: '../#/?coin=btc', + url: '../?coin=btc', }, { id: 'bch', coinName: 'Bitcoin Cash', - url: '../#/?coin=bch', + url: '../?coin=bch', }, { id: 'btg', coinName: 'Bitcoin Gold', - url: '../#/?coin=btg', + url: '../?coin=btg', }, { id: 'dash', coinName: 'Dash', - url: '../#/?coin=dash', + url: '../?coin=dash', }, { id: 'doge', coinName: 'Dogecoin', - url: '../#/?coin=doge', + url: '../?coin=doge', }, { id: 'ltc', coinName: 'Litecoin', - url: '../#/?coin=ltc', + url: '../?coin=ltc', }, { id: 'nmc', coinName: 'Namecoin', - url: '../#/?coin=nmc', + url: '../?coin=nmc', }, { id: 'vtc', coinName: 'Vertcoin', - url: '../#/?coin=vtc', + url: '../?coin=vtc', }, { id: 'zec', coinName: 'Zcash', - url: '../#/?coin=zec', + url: '../?coin=zec', }, { id: 'xem', diff --git a/src/index.html b/src/index.html index 7278523f..fc560aef 100644 --- a/src/index.html +++ b/src/index.html @@ -4,15 +4,15 @@ - Ethereum Wallet | Trezor + Trezor Wallet - - + + diff --git a/src/reducers/ModalReducer.js b/src/reducers/ModalReducer.js index a425676a..4f3168df 100644 --- a/src/reducers/ModalReducer.js +++ b/src/reducers/ModalReducer.js @@ -44,14 +44,6 @@ export default function modal(state: State = initialState, action: Action): Stat windowType: action.type, }; - // device acquired - // close modal - case DEVICE.CHANGED: - if (state.context === MODAL.CONTEXT_DEVICE && action.device.path === state.device.path && action.device.status === 'occupied') { - return initialState; - } - return state; - // device connected // close modal if modal context is not 'device' case DEVICE.CONNECT: diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 597f1b3c..2ad40df0 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -47,7 +47,9 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa switch (action.type) { case WALLET.SET_INITIAL_URL: - api.dispatch(LocalStorageActions.loadData()); + if (action.pathname) { + api.dispatch(LocalStorageActions.loadData()); + } break; case WALLET.SET_SELECTED_DEVICE: // try to authorize device diff --git a/src/utils/__tests__/__snapshots__/ethUtils.test.js.snap b/src/utils/__tests__/__snapshots__/ethUtils.test.js.snap index f5b70d96..ad90b78c 100644 --- a/src/utils/__tests__/__snapshots__/ethUtils.test.js.snap +++ b/src/utils/__tests__/__snapshots__/ethUtils.test.js.snap @@ -34,7 +34,7 @@ exports[`eth utils sanitizeHex 3`] = `"0x02"`; exports[`eth utils sanitizeHex 4`] = `"0x0100"`; -exports[`eth utils sanitizeHex 5`] = `null`; +exports[`eth utils sanitizeHex 5`] = `"0x0999"`; exports[`eth utils sanitizeHex 6`] = `""`; diff --git a/src/utils/__tests__/device.test.js b/src/utils/__tests__/device.test.js index e135a151..78281035 100644 --- a/src/utils/__tests__/device.test.js +++ b/src/utils/__tests__/device.test.js @@ -40,9 +40,9 @@ describe('device utils', () => { it('isWebUSB', () => { const data = [ - { transport: { version: ['webusb'] } }, - { transport: { version: ['aaaaaa'] } }, - { transport: { version: ['webusb', 'test'] } }, + { transport: { type: 'ParallelTransport', version: 'webusb' } }, + { transport: { type: null, version: 'aaaaaa' } }, + { transport: { type: 'ParallelTransport', version: 'webusb' } }, ]; data.forEach((item) => { @@ -52,8 +52,8 @@ describe('device utils', () => { it('isDisabled', () => { const data = [ - { selectedDevice: { features: null }, devices: [1, 2, 3], transport: { version: ['webusb', 'test'] } }, - { selectedDevice: { features: null }, devices: [], transport: { version: ['test'] } }, + { selectedDevice: { features: null }, devices: [1, 2, 3], transport: { version: 'webusb' } }, + { selectedDevice: { features: null }, devices: [], transport: { version: 'test' } }, ]; data.forEach((item) => { diff --git a/src/utils/__tests__/ethUtils.test.js b/src/utils/__tests__/ethUtils.test.js index 07bd1250..119b90e5 100644 --- a/src/utils/__tests__/ethUtils.test.js +++ b/src/utils/__tests__/ethUtils.test.js @@ -27,7 +27,7 @@ describe('eth utils', () => { }); it('sanitizeHex', () => { - const input = ['0x2540be3ff', '1', '2', '100', 999, '']; + const input = ['0x2540be3ff', '1', '2', '100', '999', '']; input.forEach((entry) => { expect(ethUtils.sanitizeHex(entry)).toMatchSnapshot(); diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 00000000..dd89c7ef --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,62 @@ +/* @flow */ + +// Copy-paste from mytrezor (old wallet) +// https://github.com/satoshilabs/mytrezor/blob/develop/app/scripts/storage/BackendLocalStorage.js +export const getStoragePath = (): string => { + const regexHash = /^([^#]*)#.*$/; + const path = window.location.href.replace(regexHash, '$1'); + const regexStart = /^[^:]*:\/\/[^/]*\//; + return path.replace(regexStart, '/'); +}; + +export const STORAGE_PATH: string = getStoragePath(); + +type StorageType = 'local' | 'session'; + +export const get: (type: StorageType, key: string) => T | null = (type, key) => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + return storage.getItem(key); + } catch (error) { + console.warn(`Get ${type} storage: ${error}`); + return null; + } +}; + +export const set = (type: StorageType, key: string, value: string | number | boolean): void => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + storage.setItem(key, value); + } catch (error) { + console.warn(`Save ${type} storage: ${error}`); + } +}; + +export const remove = (type: StorageType, key: string): void => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + storage.removeItem(key); + } catch (error) { + console.warn(`Remove ${type} storage: ${error}`); + } +}; + +export const clearAll = (type: ?StorageType): void => { + let clearLocal: boolean = true; + let clearSession: boolean = true; + if (typeof type === 'string') { + clearLocal = type === 'local'; + clearSession = !clearLocal; + } + + try { + if (clearLocal) { + Object.keys(window.localStorage).forEach(key => key.indexOf(STORAGE_PATH) >= 0 && window.localStorage.removeItem(key)); + } + if (clearSession) { + Object.keys(window.sessionStorage).forEach(key => key.indexOf(STORAGE_PATH) >= 0 && window.sessionStorage.removeItem(key)); + } + } catch (error) { + console.error(`Clearing sessionStorage error: ${error}`); + } +}; \ No newline at end of file diff --git a/src/views/Landing/views/InstallBridge/index.js b/src/views/Landing/views/InstallBridge/index.js index d0b3834b..e648ae16 100644 --- a/src/views/Landing/views/InstallBridge/index.js +++ b/src/views/Landing/views/InstallBridge/index.js @@ -135,7 +135,7 @@ class InstallBridge extends PureComponent { latestVersion: props.transport.bridge.version.join('.'), installers, target: currentTarget || installers[0], - uri: 'https://wallet.trezor.io/data/', + uri: 'https://data.trezor.io/', }; } diff --git a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js index dfd38016..f41efa2a 100644 --- a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js @@ -78,7 +78,7 @@ const AddAccountIconWrapper = styled.div` const DiscoveryStatusWrapper = styled.div` display: flex; flex-direction: column; - + width: 100%; font-size: ${FONT_SIZE.SMALL}; padding: ${LEFT_NAVIGATION_ROW.PADDING}; white-space: nowrap; @@ -164,15 +164,34 @@ const AccountMenu = (props: Props) => { let discoveryStatus = null; const discovery = props.discovery.find(d => d.deviceState === selected.state && d.network === location.state.network); - if (discovery) { - if (discovery.completed) { - // TODO: add only if last one is not empty - //if (selectedAccounts.length > 0 && selectedAccounts[selectedAccounts.length - 1]) - const lastAccount = deviceAccounts[deviceAccounts.length - 1]; - if (lastAccount && (new BigNumber(lastAccount.balance).greaterThan(0) || lastAccount.nonce > 0)) { - discoveryStatus = ( - - + if (discovery && discovery.completed) { + // TODO: add only if last one is not empty + //if (selectedAccounts.length > 0 && selectedAccounts[selectedAccounts.length - 1]) + const lastAccount = deviceAccounts[deviceAccounts.length - 1]; + if (lastAccount && (new BigNumber(lastAccount.balance).greaterThan(0) || lastAccount.nonce > 0)) { + discoveryStatus = ( + + + + + + Add account + + + ); + } else { + discoveryStatus = ( + To add a new account, last account must have some transactions.} + placement="bottom" + > + + { Add account - ); - } else { - discoveryStatus = ( - To add a new account, last account must have some transactions.} - placement="bottom" - > - - - - - - Add account - - - - ); - } - } else if (!selected.connected || !selected.available) { - discoveryStatus = ( - - - Accounts could not be loaded - - {`Connect ${selected.instanceLabel} device`} - - - - ); - } else { - discoveryStatus = ( - - - - - Loading... - - - + ); } + } else if (!selected.connected) { + discoveryStatus = ( + + + Accounts could not be loaded + + {`Connect ${selected.instanceLabel} device`} + + + + ); + } else { + discoveryStatus = ( + + + + + Loading... + + + + ); } return ( diff --git a/src/views/Wallet/components/TopNavigationAccount/components/Indicator/index.js b/src/views/Wallet/components/TopNavigationAccount/components/Indicator/index.js index 9a414a7d..0e5eda43 100644 --- a/src/views/Wallet/components/TopNavigationAccount/components/Indicator/index.js +++ b/src/views/Wallet/components/TopNavigationAccount/components/Indicator/index.js @@ -6,7 +6,7 @@ import React, { PureComponent } from 'react'; type Props = { pathname: string; - wrapper: ?HTMLElement; + wrapper: () => ?HTMLElement; } type State = { @@ -71,8 +71,8 @@ class Indicator extends PureComponent { handleResize: () => void; reposition(resetAnimation: boolean = true) { - if (!this.props.wrapper) return; - const { wrapper } = this.props; + const wrapper = this.props.wrapper(); + if (!wrapper) return; const active = wrapper.querySelector('.active'); if (!active) return; const bounds = active.getBoundingClientRect(); diff --git a/src/views/Wallet/components/TopNavigationAccount/index.js b/src/views/Wallet/components/TopNavigationAccount/index.js index d016a718..239fffc0 100644 --- a/src/views/Wallet/components/TopNavigationAccount/index.js +++ b/src/views/Wallet/components/TopNavigationAccount/index.js @@ -65,7 +65,7 @@ class TopNavigationAccount extends React.PureComponent { Receive Send Sign & Verify - + this.wrapper} /> ); } diff --git a/src/views/Wallet/views/Account/Send/index.js b/src/views/Wallet/views/Account/Send/index.js index c5a2fbdb..e86bf298 100644 --- a/src/views/Wallet/views/Account/Send/index.js +++ b/src/views/Wallet/views/Account/Send/index.js @@ -105,6 +105,7 @@ const UpdateFeeWrapper = styled.span` const StyledLink = styled(Link)` margin-left: 4px; + white-space: nowrap; `; const ToggleAdvancedSettingsWrapper = styled.div` diff --git a/src/views/Wallet/views/Account/Summary/index.js b/src/views/Wallet/views/Account/Summary/index.js index 3c88ed14..f725a44d 100644 --- a/src/views/Wallet/views/Account/Summary/index.js +++ b/src/views/Wallet/views/Account/Summary/index.js @@ -13,6 +13,7 @@ import Content from 'views/Wallet/components/Content'; import CoinLogo from 'components/images/CoinLogo'; import * as stateUtils from 'reducers/utils'; import Link from 'components/Link'; +import { FONT_WEIGHT, FONT_SIZE } from 'config/variables'; import AccountBalance from './components/Balance'; import AddedToken from './components/Token'; @@ -38,9 +39,16 @@ const StyledTooltip = styled(Tooltip)` const AccountName = styled.div` display: flex; + justify-content: center; align-items: center; `; +const AccountTitle = styled.div` + font-size: ${FONT_SIZE.WALLET_TITLE}; + font-weight: ${FONT_WEIGHT.BASE}; + color: ${colors.WALLET_TITLE}; +`; + const StyledCoinLogo = styled(CoinLogo)` margin-right: 10px; `; @@ -83,7 +91,7 @@ const AccountSummary = (props: Props) => { -

Account #{parseInt(account.index, 10) + 1}

+ Account #{parseInt(account.index, 10) + 1}
See full transaction history
diff --git a/yarn.lock b/yarn.lock index 9d9e3b69..80c94695 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2570,6 +2570,10 @@ commander@^2.13.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" +commander@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + commander@^2.8.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -10217,9 +10221,9 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trezor-connect@^5.0.32: - version "5.0.32" - resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-5.0.32.tgz#a8077f46653fec16d8dc25358e157ed1048a4def" +trezor-connect@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.0.tgz#2a45336f29a4a3f2a8ad2d121363b0e7a1b767ef" dependencies: babel-runtime "^6.26.0" events "^1.1.1"