mirror of
https://github.com/trezor/trezor-wallet
synced 2024-11-23 16:58:28 +00:00
Merge branch 'master' into unupported-browsers
This commit is contained in:
commit
a15ae3b0ac
@ -1,4 +1,6 @@
|
||||
public
|
||||
build
|
||||
build-devel
|
||||
coverage
|
||||
images
|
||||
node_modules
|
||||
|
@ -12,6 +12,7 @@
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"import/prefer-default-export": 0,
|
||||
"no-use-before-define": 0,
|
||||
"no-plusplus": 0,
|
||||
"class-methods-use-this": 0,
|
||||
|
@ -43,4 +43,5 @@ module.name_mapper='^data' -> '<PROJECT_ROOT>/src/data'
|
||||
module.name_mapper='^services' -> '<PROJECT_ROOT>/src/services'
|
||||
module.name_mapper='^support' -> '<PROJECT_ROOT>/src/support'
|
||||
module.name_mapper='^public' -> '<PROJECT_ROOT>/public'
|
||||
module.name_mapper='^images' -> '<PROJECT_ROOT>/src/images'
|
||||
module.system=haste
|
||||
|
12
package.json
12
package.json
@ -10,8 +10,8 @@
|
||||
"scripts": {
|
||||
"dev": "npx webpack-dev-server --config ./webpack/dev.babel.js",
|
||||
"dev:local": "npx webpack-dev-server --config ./webpack/local.babel.js",
|
||||
"build:clean": "rm -rf build",
|
||||
"build:production": "npx webpack --config ./webpack/production.babel.js --progress --bail",
|
||||
"build:prod": "rimraf build && npx webpack --config ./webpack/production.babel.js --progress --bail",
|
||||
"build:dev": "rimraf build-devel && cross-env BUILD=development npx webpack --config ./webpack/production.babel.js --output-path build-devel --progress --bail",
|
||||
"build": "run-s build:*",
|
||||
"flow": "flow check src",
|
||||
"lint": "run-s lint:*",
|
||||
@ -19,7 +19,9 @@
|
||||
"lint:css": "npx stylelint './src/**/*.js'",
|
||||
"test": "run-s test:*",
|
||||
"test:unit": "npx jest",
|
||||
"test-unit:watch": "npx jest -o --watch"
|
||||
"test-unit:watch": "npx jest -o --watch",
|
||||
"prod-server": "npx http-server ./build -a localhost -S -C ./server/cert.pem -K ./server/key.pem -o",
|
||||
"prod-server-dev": "npx http-server ./build-devel -a localhost -S -C ./server/cert.pem -K ./server/key.pem -o"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel": "^6.23.0",
|
||||
@ -27,6 +29,7 @@
|
||||
"bignumber.js": "2.4.0",
|
||||
"color-hash": "^1.0.3",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"ethereumjs-tx": "^1.3.3",
|
||||
"ethereumjs-units": "^0.2.0",
|
||||
@ -35,6 +38,7 @@
|
||||
"git-revision-webpack-plugin": "^3.0.3",
|
||||
"hdkey": "^0.8.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^0.11.1",
|
||||
"jest-fetch-mock": "^1.6.5",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"prop-types": "^15.6.2",
|
||||
@ -57,11 +61,13 @@
|
||||
"redux-raven-middleware": "^1.2.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"styled-components": "^3.4.9",
|
||||
"rimraf": "^2.6.2",
|
||||
"styled-media-query": "^2.0.2",
|
||||
"styled-normalize": "^8.0.0",
|
||||
"trezor-connect": "^5.0.32",
|
||||
"web3": "1.0.0-beta.35",
|
||||
"webpack": "^4.16.3",
|
||||
"webpack-build-notifier": "^0.1.29",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"whatwg-fetch": "^2.0.4",
|
||||
"yarn-run-all": "^3.1.1"
|
||||
|
20
server/cert.pem
Normal file
20
server/cert.pem
Normal file
@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDMjCCAhoCCQD/Ey0A0Ll1FjANBgkqhkiG9w0BAQsFADBbMQswCQYDVQQGEwJj
|
||||
ejEKMAgGA1UECAwBczEKMAgGA1UEBwwBYTEKMAgGA1UECgwBczEKMAgGA1UECwwB
|
||||
czEKMAgGA1UEAwwBczEQMA4GCSqGSIb3DQEJARYBczAeFw0xODA3MjUxMTUyMzla
|
||||
Fw0xODA4MjQxMTUyMzlaMFsxCzAJBgNVBAYTAmN6MQowCAYDVQQIDAFzMQowCAYD
|
||||
VQQHDAFhMQowCAYDVQQKDAFzMQowCAYDVQQLDAFzMQowCAYDVQQDDAFzMRAwDgYJ
|
||||
KoZIhvcNAQkBFgFzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1sON
|
||||
vf+ny+KASV8pCT83fJNyAV8uAvg/Ur0B/bf26onvhoUJ74w6lFfJTyw0WjOzXR/G
|
||||
reXg4yO8aGHTwSuYifY0Uj1Lm4cZYz3n96X37cKyviNg8zp+QXKgESwnxJ/VyLf+
|
||||
rYc/DzTQsXFs7irDXsGCq+8z56ljZ788axd5wip2AGBAnTzH9OeHD8sRIYNqFKNm
|
||||
S2Rx1Ev3QIIwqng3e6vX4/kk7Zz380z1MpB2wbEsUQxx27hT4Z2tZRyn0L/AXQBL
|
||||
H3zxA5FNVxgQaUAFWoqS6cv8nJtIRbnvoiwNygDMyxhrB+CbUCtCVDm8W94TE+Qn
|
||||
+BdZzfPg/fQbv4B6owIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC/g7yIDluDPOKP
|
||||
mo37+hI4vQT90HnSzccMhelBoqHSw2OFN3O+eantCy9eNr9UNHeZP7KyTsvRW5nW
|
||||
L48Ps1eAPC66QCNvkLmpJ3YRHa157722cPdgV0jYm5njV+9pdjNFFD/yF43nNm/r
|
||||
IdiOwXak4qqajDeug9oeZh/sGPZ888gnZMaYhON8zzL+CR+xKv45ORVpHY+fQp4f
|
||||
xv8195pqcehFy2RYniDzcU/cy3IfOJaxYYbV5mlQtFvY8plhC3QtcJebS0nei4J+
|
||||
oX2Wh1dxKUNGxqPUjwfyYxPtxPTUEsOJAtTtwcrQ2ShKCzFSP9xdZ9i4WviV84pU
|
||||
9RGGV7yC
|
||||
-----END CERTIFICATE-----
|
28
server/key.pem
Normal file
28
server/key.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWw429/6fL4oBJ
|
||||
XykJPzd8k3IBXy4C+D9SvQH9t/bqie+GhQnvjDqUV8lPLDRaM7NdH8at5eDjI7xo
|
||||
YdPBK5iJ9jRSPUubhxljPef3pfftwrK+I2DzOn5BcqARLCfEn9XIt/6thz8PNNCx
|
||||
cWzuKsNewYKr7zPnqWNnvzxrF3nCKnYAYECdPMf054cPyxEhg2oUo2ZLZHHUS/dA
|
||||
gjCqeDd7q9fj+STtnPfzTPUykHbBsSxRDHHbuFPhna1lHKfQv8BdAEsffPEDkU1X
|
||||
GBBpQAVaipLpy/ycm0hFue+iLA3KAMzLGGsH4JtQK0JUObxb3hMT5Cf4F1nN8+D9
|
||||
9Bu/gHqjAgMBAAECggEBAIbMgXgjMn/vcBQdjZVHP52KsoEX67pjdOOKzOgigvHd
|
||||
mCE3+e+IdfBMVYfDOCzxzIAEBOF7qzcGZCikVpQlt/3IMjj4Ti+VkaLP5Xx0iPSM
|
||||
Q0LC1AR2z25m8v80VtW8eSQeENV8UWFLBj6J8hRfdPdRwKIIZuzeTg19Y//X4U2z
|
||||
0AQHc1askJc2Hu+FEm5GajurR4CEawrgHXtBaY8F96lGPRWXJThZhB8GemKkN8Ws
|
||||
i8uMgdB4dcO7iGa1GpiSErCbJxC74/FA2XKf9eVYL7tRm1Bc/hojl1FxbWVnlC+W
|
||||
RXqhdcKTHQ6LODYUovBCp5A5p5S/+zB24tnMgfIhYikCgYEA+1bzT4P2XuP4/A2N
|
||||
yH3Da/fWlsoVo7/c9vy0AQhWo/ZH9mHiP1A7qC6b98T2DL0qsv0viGKBKPw0J1Cj
|
||||
R4Tn3aKwPxsceHOEZ9zh1qApvaQH2kVMYrxbGCYGMIcj2Ps6HjPJGy3wH5T1+sco
|
||||
u0qhIKs07TtoQe767HV+XWohxacCgYEA2r78nPhKpAQDiJ1uuqNPHybttnHX2OTv
|
||||
f/LCM3B4pmORCSVostjqN6yUAUYBjvEczCkcyW3mTim7H9C92bePZmRrC8/7csYm
|
||||
2ymJAzRMFlbnjIzF3EkBTcWermcrXwBchDQ6LlaKATT3qbxlewplFTE7h/3wa7da
|
||||
kUPwHbJx+qUCgYAZCwbfS2TG+6wZYThZW76XCXDGQYh6cmmP6on8+Fm5qJZvBD3I
|
||||
1TO8hDhiLavehRK2FugfjMEV1ltT94LtY16/BLDO+OKTVd9Bgg62lerSzH9DzlfY
|
||||
FrB07YT8XNrDifS2ga5uGNuuKeeAf0udrcf0O1rgsGSo/SjfWq2mnSaUTQKBgHZl
|
||||
iTUs7rl3srHvBE/gtKKX33IwjDPJNhh6vMI6zhLBMW9R4CltXthjgHhv+8fymTOn
|
||||
zPz5jv4feDjwMtH0mJlDIO1z1RV6Su20vYQOemBdCVb5mt5wZVRC8nBTRxZUi77C
|
||||
xfruvCOLF8G3RvYh2jRuQVqKB+dFhq+5pe1s+GRBAoGBAKX8oBMbm3e2eC3FzCKE
|
||||
sB4BKLzGnQmoU4E/xUYLU+h5TTht4orsCil4Y9kkIE+jPP4Z84xqpfwN4QHw4Ls+
|
||||
SwBi+3C0HIo2Evss1D4ELs58N9nNnJRsGkqBqQkJIygpUVgYOTsCBOmjjm/5+Cd3
|
||||
RUE7p2ym8WCZmDkVkhFmZk1J
|
||||
-----END PRIVATE KEY-----
|
@ -1,7 +1,7 @@
|
||||
/* @flow */
|
||||
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import type { Action, TrezorDevice } from 'flowtype';
|
||||
import type { Action } from 'flowtype';
|
||||
import type { Account, State } from 'reducers/AccountsReducer';
|
||||
|
||||
export type AccountFromStorageAction = {
|
||||
@ -60,5 +60,5 @@ export const setNonce = (address: string, network: string, deviceState: string,
|
||||
|
||||
export const update = (account: Account): Action => ({
|
||||
type: ACCOUNT.UPDATE,
|
||||
payload: account
|
||||
payload: account,
|
||||
});
|
||||
|
@ -1,42 +1,26 @@
|
||||
/* @flow */
|
||||
|
||||
import Web3 from 'web3';
|
||||
import HDKey from 'hdkey';
|
||||
|
||||
import EthereumjsUtil from 'ethereumjs-util';
|
||||
import EthereumjsUnits from 'ethereumjs-units';
|
||||
import EthereumjsTx from 'ethereumjs-tx';
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import type { EstimateGasOptions } from 'web3';
|
||||
import type { TransactionStatus, TransactionReceipt } from 'web3';
|
||||
import { strip } from 'utils/ethUtils';
|
||||
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
||||
import * as WEB3 from 'actions/constants/web3';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
|
||||
import * as AccountsActions from './AccountsActions';
|
||||
import * as Web3Actions from './Web3Actions';
|
||||
|
||||
import type {
|
||||
TrezorDevice,
|
||||
Dispatch,
|
||||
GetState,
|
||||
Action,
|
||||
AsyncAction,
|
||||
PromiseAction,
|
||||
ThunkAction,
|
||||
} from 'flowtype';
|
||||
import type { EthereumAccount } from 'trezor-connect';
|
||||
import type { Token } from 'reducers/TokensReducer';
|
||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||
import * as Web3Actions from './Web3Actions';
|
||||
import * as AccountsActions from './AccountsActions';
|
||||
|
||||
export type BlockchainAction = {
|
||||
type: typeof BLOCKCHAIN.READY,
|
||||
}
|
||||
|
||||
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumAccount> => {
|
||||
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
|
||||
// get data from connect
|
||||
// Temporary disabled, enable after trezor-connect@5.0.32 release
|
||||
const txs = await TrezorConnect.ethereumGetAccountInfo({
|
||||
@ -44,8 +28,8 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
||||
address,
|
||||
block: 0,
|
||||
transactions: 0,
|
||||
balance: "0",
|
||||
nonce: 0
|
||||
balance: '0',
|
||||
nonce: 0,
|
||||
},
|
||||
coin: network,
|
||||
});
|
||||
@ -55,9 +39,9 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
||||
}
|
||||
|
||||
// blockbook web3 fallback
|
||||
const web3account = await dispatch( Web3Actions.discoverAccount(address, network) );
|
||||
const web3account = await dispatch(Web3Actions.discoverAccount(address, network));
|
||||
// return { transactions: txs.payload, ...web3account };
|
||||
return {
|
||||
return {
|
||||
address,
|
||||
transactions: txs.payload.transactions,
|
||||
block: txs.payload.block,
|
||||
@ -66,35 +50,53 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
||||
};
|
||||
};
|
||||
|
||||
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
|
||||
return await dispatch( Web3Actions.getTokenInfo(input, network) );
|
||||
}
|
||||
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => dispatch(Web3Actions.getTokenInfo(input, network));
|
||||
|
||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||
return await dispatch( Web3Actions.getTokenBalance(token) );
|
||||
}
|
||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => dispatch(Web3Actions.getTokenBalance(token));
|
||||
|
||||
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch, getState: GetState): Promise<BigNumber> => {
|
||||
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch): Promise<BigNumber> => {
|
||||
try {
|
||||
const gasPrice = await dispatch( Web3Actions.getCurrentGasPrice(network) );
|
||||
const gasPrice = await dispatch(Web3Actions.getCurrentGasPrice(network));
|
||||
return new BigNumber(gasPrice);
|
||||
} catch (error) {
|
||||
return new BigNumber(defaultGasPrice);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
|
||||
return await dispatch( Web3Actions.estimateGasLimit(network, { to: '', data, value, gasPrice }) );
|
||||
}
|
||||
const estimateProxy: Array<Promise<string>> = [];
|
||||
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||
// Since this method could be called multiple times in short period of time
|
||||
// check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first)
|
||||
// TODO: should reject second call immediately?
|
||||
if (estimateProxy.length > 0) {
|
||||
// wait for proxy result (but do not process it)
|
||||
await estimateProxy[0];
|
||||
}
|
||||
|
||||
const call = dispatch(Web3Actions.estimateGasLimit(network, {
|
||||
to: '',
|
||||
data,
|
||||
value,
|
||||
gasPrice,
|
||||
}));
|
||||
// add current call to proxy
|
||||
estimateProxy.push(call);
|
||||
// wait for result
|
||||
const result = await call;
|
||||
// remove current call from proxy
|
||||
estimateProxy.splice(0, 1);
|
||||
// return result
|
||||
return result;
|
||||
};
|
||||
|
||||
export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
||||
const network: string = coinInfo.shortcut.toLowerCase();
|
||||
|
||||
// try to resolve pending transactions
|
||||
await dispatch( Web3Actions.resolvePendingTransactions(network) );
|
||||
await dispatch(Web3Actions.resolvePendingTransactions(network));
|
||||
|
||||
await dispatch( Web3Actions.updateGasPrice(network) );
|
||||
await dispatch(Web3Actions.updateGasPrice(network));
|
||||
|
||||
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
|
||||
if (accounts.length > 0) {
|
||||
@ -103,39 +105,37 @@ export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispa
|
||||
accounts,
|
||||
coin: network,
|
||||
});
|
||||
|
||||
|
||||
if (response.success) {
|
||||
response.payload.forEach((a, i) => {
|
||||
if (a.transactions > 0) {
|
||||
// load additional data from Web3 (balance, nonce, tokens)
|
||||
dispatch( Web3Actions.updateAccount(accounts[i], a, network) )
|
||||
dispatch(Web3Actions.updateAccount(accounts[i], a, network));
|
||||
} else {
|
||||
// there are no new txs, just update block
|
||||
dispatch( AccountsActions.update( { ...accounts[i], block: a.block }) );
|
||||
dispatch(AccountsActions.update({ ...accounts[i], block: a.block }));
|
||||
|
||||
// HACK: since blockbook can't work with smart contracts for now
|
||||
// try to update tokens balances added to this account using Web3
|
||||
dispatch( Web3Actions.updateAccountTokens(accounts[i]) );
|
||||
dispatch(Web3Actions.updateAccountTokens(accounts[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// not used for now, waiting for fix in blockbook
|
||||
/*
|
||||
export const onNotification = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
|
||||
// this event can be triggered multiple times
|
||||
// 1. check if pair [txid + address] is already in reducer
|
||||
const network: string = payload.coin.shortcut.toLowerCase();
|
||||
const address: string = EthereumjsUtil.toChecksumAddress(payload.tx.address);
|
||||
const txInfo = await dispatch( Web3Actions.getPendingInfo(network, payload.tx.txid) );
|
||||
const txInfo = await dispatch(Web3Actions.getPendingInfo(network, payload.tx.txid));
|
||||
|
||||
// const exists = getState().pending.filter(p => p.id === payload.tx.txid && p.address === address);
|
||||
const exists = getState().pending.filter(p => {
|
||||
return p.address === address;
|
||||
});
|
||||
const exists = getState().pending.filter(p => p.address === address);
|
||||
if (exists.length < 1) {
|
||||
if (txInfo) {
|
||||
dispatch({
|
||||
@ -144,14 +144,14 @@ export const onNotification = (payload: any): PromiseAction<void> => async (disp
|
||||
type: 'send',
|
||||
id: payload.tx.txid,
|
||||
network,
|
||||
currency: "tETH",
|
||||
currency: 'tETH',
|
||||
amount: txInfo.value,
|
||||
total: "0",
|
||||
total: '0',
|
||||
tx: {},
|
||||
nonce: txInfo.nonce,
|
||||
address,
|
||||
rejected: false
|
||||
}
|
||||
rejected: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// tx info not found (yet?)
|
||||
@ -164,18 +164,18 @@ export const onNotification = (payload: any): PromiseAction<void> => async (disp
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
|
||||
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const addresses: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address);
|
||||
// $FlowIssue: trezor-connect@5.0.32
|
||||
return await TrezorConnect.blockchainSubscribe({
|
||||
const addresses: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address); // eslint-disable-line no-unused-vars
|
||||
await TrezorConnect.blockchainSubscribe({
|
||||
// accounts: addresses,
|
||||
accounts: [],
|
||||
coin: network
|
||||
coin: network,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Conditionally subscribe to blockchain backend
|
||||
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
||||
@ -185,26 +185,26 @@ export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getSta
|
||||
if (getState().discovery.length > 0) {
|
||||
// get unique networks
|
||||
const networks: Array<string> = [];
|
||||
getState().discovery.forEach(discovery => {
|
||||
getState().discovery.forEach((discovery) => {
|
||||
if (networks.indexOf(discovery.network) < 0) {
|
||||
networks.push(discovery.network);
|
||||
}
|
||||
});
|
||||
|
||||
// subscribe
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
await dispatch( subscribe(networks[i]) );
|
||||
}
|
||||
const results = networks.map(n => dispatch(subscribe(n)));
|
||||
// wait for all subscriptions
|
||||
await Promise.all(results);
|
||||
}
|
||||
|
||||
// continue wallet initialization
|
||||
dispatch({
|
||||
type: BLOCKCHAIN.READY
|
||||
type: BLOCKCHAIN.READY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
||||
// disconnect and remove Web3 webscocket instance if exists
|
||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
dispatch( Web3Actions.disconnect(payload.coin) );
|
||||
}
|
||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
dispatch(Web3Actions.disconnect(payload.coin));
|
||||
};
|
@ -9,10 +9,7 @@ import type {
|
||||
ThunkAction, AsyncAction, PromiseAction, Action, GetState, Dispatch, TrezorDevice,
|
||||
} from 'flowtype';
|
||||
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
||||
import * as AccountsActions from './AccountsActions';
|
||||
import * as BlockchainActions from './BlockchainActions';
|
||||
import { setBalance as setTokenBalance } from './TokenActions';
|
||||
|
||||
|
||||
export type DiscoveryStartAction = {
|
||||
type: typeof DISCOVERY.START,
|
||||
@ -65,7 +62,7 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
|
||||
return;
|
||||
}
|
||||
|
||||
const discovery: State = getState().discovery;
|
||||
const { discovery } = getState();
|
||||
const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network);
|
||||
|
||||
if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) {
|
||||
@ -88,7 +85,7 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
|
||||
}
|
||||
|
||||
if (!discoveryProcess) {
|
||||
dispatch(begin(device, network))
|
||||
dispatch(begin(device, network));
|
||||
} else if (discoveryProcess.completed && !ignoreCompleted) {
|
||||
dispatch({
|
||||
type: DISCOVERY.COMPLETE,
|
||||
@ -172,9 +169,8 @@ const begin = (device: TrezorDevice, network: string): AsyncAction => async (dis
|
||||
dispatch(start(device, network));
|
||||
};
|
||||
|
||||
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
||||
const { completed } = discoveryProcess;
|
||||
discoveryProcess.completed = false;
|
||||
|
||||
const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`);
|
||||
const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex);
|
||||
@ -183,15 +179,13 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
|
||||
const { network } = discoveryProcess;
|
||||
|
||||
// TODO: check if address was created before
|
||||
|
||||
try {
|
||||
const account = await dispatch( BlockchainActions.discoverAccount(device, ethAddress, network) );
|
||||
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network));
|
||||
if (discoveryProcess.interrupted) return;
|
||||
|
||||
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
|
||||
const accountIsEmpty = account.nonce <= 0 && account.balance === '0';
|
||||
if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) {
|
||||
|
||||
dispatch({
|
||||
type: ACCOUNT.CREATE,
|
||||
payload: {
|
||||
@ -205,22 +199,20 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
|
||||
balance: account.balance,
|
||||
nonce: account.nonce,
|
||||
block: account.block,
|
||||
transactions: account.transactions
|
||||
}
|
||||
transactions: account.transactions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (accountIsEmpty) {
|
||||
dispatch( finish(device, discoveryProcess) );
|
||||
} else {
|
||||
if (!completed) { dispatch( discoverAccount(device, discoveryProcess) ); }
|
||||
dispatch(finish(device, discoveryProcess));
|
||||
} else if (!completed) {
|
||||
dispatch(discoverAccount(device, discoveryProcess));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
dispatch({
|
||||
type: DISCOVERY.STOP,
|
||||
device
|
||||
device,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
@ -243,7 +235,7 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
|
||||
}
|
||||
};
|
||||
|
||||
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
||||
await TrezorConnect.getFeatures({
|
||||
device: {
|
||||
path: device.path,
|
||||
@ -254,7 +246,7 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction
|
||||
useEmptyPassphrase: !device.instance,
|
||||
});
|
||||
|
||||
await dispatch( BlockchainActions.subscribe(discoveryProcess.network) );
|
||||
await dispatch(BlockchainActions.subscribe(discoveryProcess.network));
|
||||
|
||||
if (discoveryProcess.interrupted) return;
|
||||
|
||||
@ -263,13 +255,12 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction
|
||||
device,
|
||||
network: discoveryProcess.network,
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export const reconnect = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
export const reconnect = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
await dispatch(BlockchainActions.subscribe(network));
|
||||
dispatch(restore());
|
||||
}
|
||||
};
|
||||
|
||||
export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const selected = getState().wallet.selectedDevice;
|
||||
|
@ -25,7 +25,7 @@ export const onPinSubmit = (value: string): Action => {
|
||||
};
|
||||
|
||||
export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
||||
const resp = await TrezorConnect.uiResponse({
|
||||
await TrezorConnect.uiResponse({
|
||||
type: UI.RECEIVE_PASSPHRASE,
|
||||
payload: {
|
||||
value: passphrase,
|
||||
|
@ -48,7 +48,7 @@ export const showUnverifiedAddress = (): Action => ({
|
||||
});
|
||||
|
||||
//export const showAddress = (address_n: string): AsyncAction => {
|
||||
export const showAddress = (address_n: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
export const showAddress = (path: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const selected = getState().wallet.selectedDevice;
|
||||
if (!selected) return;
|
||||
|
||||
@ -66,7 +66,7 @@ export const showAddress = (address_n: Array<number>): AsyncAction => async (dis
|
||||
instance: selected.instance,
|
||||
state: selected.state,
|
||||
},
|
||||
path: address_n,
|
||||
path,
|
||||
useEmptyPassphrase: !selected.instance,
|
||||
});
|
||||
|
||||
@ -90,7 +90,7 @@ export const showAddress = (address_n: Array<number>): AsyncAction => async (dis
|
||||
{
|
||||
label: 'Try again',
|
||||
callback: () => {
|
||||
dispatch(showAddress(address_n));
|
||||
dispatch(showAddress(path));
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -3,13 +3,9 @@
|
||||
|
||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
|
||||
import * as SendFormActions from 'actions/SendFormActions';
|
||||
import * as SessionStorageActions from 'actions/SessionStorageActions';
|
||||
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
|
||||
import type {
|
||||
@ -36,17 +32,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|
||||
const state: State = getState();
|
||||
const { location } = state.router;
|
||||
|
||||
// reset form to default
|
||||
if (action.type === SEND.TX_COMPLETE) {
|
||||
// dispatch( SendFormActions.init() );
|
||||
// linear action
|
||||
// SessionStorageActions.clear(location.pathname);
|
||||
}
|
||||
|
||||
if (prevState.sendForm !== state.sendForm) {
|
||||
dispatch(SessionStorageActions.save());
|
||||
}
|
||||
|
||||
// handle devices state change (from trezor-connect events or location change)
|
||||
if (locationChange
|
||||
|| prevState.accounts !== state.accounts
|
||||
@ -85,11 +70,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|
||||
payload,
|
||||
});
|
||||
|
||||
// initialize SendFormReducer
|
||||
if (location.state.send && getState().sendForm.currency === '') {
|
||||
dispatch(SendFormActions.init());
|
||||
}
|
||||
|
||||
if (location.state.send) {
|
||||
const rejectedTxs = pending.filter(tx => tx.rejected);
|
||||
rejectedTxs.forEach((tx) => {
|
||||
|
File diff suppressed because it is too large
Load Diff
413
src/actions/SendFormValidationActions.js
Normal file
413
src/actions/SendFormValidationActions.js
Normal file
@ -0,0 +1,413 @@
|
||||
/* @flow */
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import EthereumjsUtil from 'ethereumjs-util';
|
||||
import EthereumjsUnits from 'ethereumjs-units';
|
||||
import { findToken } from 'reducers/TokensReducer';
|
||||
import { findDevice, getPendingAmount } from 'reducers/utils';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
PayloadAction,
|
||||
} from 'flowtype';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
||||
|
||||
// general regular expressions
|
||||
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
|
||||
const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$');
|
||||
const ABS_RE = new RegExp('^[0-9]+$');
|
||||
const ETH_18_RE = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$');
|
||||
const HEX_RE = new RegExp('^[0-9A-Fa-f]+$');
|
||||
const dynamicRegexp = (decimals: number): RegExp => {
|
||||
if (decimals > 0) {
|
||||
return new RegExp(`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`);
|
||||
}
|
||||
return ABS_RE;
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from SendFormActions.observe
|
||||
* Reaction for WEB3.GAS_PRICE_UPDATED action
|
||||
*/
|
||||
export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => {
|
||||
// testing random data
|
||||
// function getRandomInt(min, max) {
|
||||
// return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
// }
|
||||
// const newPrice = getRandomInt(10, 50).toString();
|
||||
|
||||
const state = getState().sendForm;
|
||||
if (network === state.networkSymbol) return;
|
||||
|
||||
// check if new price is different then currently recommended
|
||||
const newPrice: string = EthereumjsUnits.convert(gasPrice, 'wei', 'gwei');
|
||||
|
||||
if (newPrice !== state.recommendedGasPrice) {
|
||||
if (!state.untouched) {
|
||||
// if there is a transaction draft let the user know
|
||||
// and let him update manually
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
state: {
|
||||
...state,
|
||||
gasPriceNeedsUpdate: true,
|
||||
recommendedGasPrice: newPrice,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// automatically update feeLevels and gasPrice
|
||||
const feeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit);
|
||||
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
state: {
|
||||
...state,
|
||||
gasPriceNeedsUpdate: false,
|
||||
recommendedGasPrice: newPrice,
|
||||
gasPrice: selectedFeeLevel.gasPrice,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Recalculate amount, total and fees
|
||||
*/
|
||||
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
// clone deep nested object
|
||||
// to avoid overrides across state history
|
||||
let state: State = JSON.parse(JSON.stringify(getState().sendForm));
|
||||
// reset errors
|
||||
state.errors = {};
|
||||
state.warnings = {};
|
||||
state.infos = {};
|
||||
state = dispatch(recalculateTotalAmount(state));
|
||||
state = dispatch(updateCustomFeeLabel(state));
|
||||
state = dispatch(addressValidation(state));
|
||||
state = dispatch(addressLabel(state));
|
||||
state = dispatch(amountValidation(state));
|
||||
state = dispatch(gasLimitValidation(state));
|
||||
state = dispatch(gasPriceValidation(state));
|
||||
state = dispatch(nonceValidation(state));
|
||||
state = dispatch(dataValidation(state));
|
||||
return state;
|
||||
};
|
||||
|
||||
export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const {
|
||||
account,
|
||||
tokens,
|
||||
pending,
|
||||
} = getState().selectedAccount;
|
||||
if (!account) return $state;
|
||||
|
||||
const state = { ...$state };
|
||||
const isToken = state.currency !== state.networkSymbol;
|
||||
|
||||
if (state.setMax) {
|
||||
const pendingAmount = getPendingAmount(pending, state.currency, isToken);
|
||||
if (isToken) {
|
||||
const token = findToken(tokens, account.address, state.currency, account.deviceState);
|
||||
if (token) {
|
||||
state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10);
|
||||
}
|
||||
} else {
|
||||
const b = new BigNumber(account.balance).minus(pendingAmount);
|
||||
state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit);
|
||||
}
|
||||
}
|
||||
|
||||
state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit);
|
||||
return state;
|
||||
};
|
||||
|
||||
export const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (): State => {
|
||||
const state = { ...$state };
|
||||
if ($state.selectedFeeLevel.value === 'Custom') {
|
||||
state.selectedFeeLevel = {
|
||||
...state.selectedFeeLevel,
|
||||
gasPrice: state.gasPrice,
|
||||
label: `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Address value validation
|
||||
*/
|
||||
export const addressValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.address) return state;
|
||||
|
||||
const { address } = state;
|
||||
|
||||
if (address.length < 1) {
|
||||
state.errors.address = 'Address is not set';
|
||||
} else if (!EthereumjsUtil.isValidAddress(address)) {
|
||||
state.errors.address = 'Address is not valid';
|
||||
} else if (address.match(UPPERCASE_RE) && !EthereumjsUtil.isValidChecksumAddress(address)) {
|
||||
state.errors.address = 'Address is not a valid checksum';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Address label assignation
|
||||
*/
|
||||
export const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.address || state.errors.address) return state;
|
||||
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
if (!account || !network) return state;
|
||||
const { address } = state;
|
||||
|
||||
const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === address.toLowerCase());
|
||||
if (savedAccounts.length > 0) {
|
||||
// check if found account belongs to this network
|
||||
const currentNetworkAccount = savedAccounts.find(a => a.network === network.network);
|
||||
if (currentNetworkAccount) {
|
||||
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState);
|
||||
if (device) {
|
||||
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`;
|
||||
}
|
||||
} else {
|
||||
// corner-case: the same derivation path is used on different networks
|
||||
const otherNetworkAccount = savedAccounts[0];
|
||||
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState);
|
||||
const { coins } = getState().localStorage.config;
|
||||
const otherNetwork = coins.find(c => c.network === otherNetworkAccount.network);
|
||||
if (device && otherNetwork) {
|
||||
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Amount value validation
|
||||
*/
|
||||
export const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.amount) return state;
|
||||
|
||||
const {
|
||||
account,
|
||||
tokens,
|
||||
pending,
|
||||
} = getState().selectedAccount;
|
||||
if (!account) return state;
|
||||
|
||||
const { amount } = state;
|
||||
if (amount.length < 1) {
|
||||
state.errors.amount = 'Amount is not set';
|
||||
} else if (amount.length > 0 && !amount.match(NUMBER_RE)) {
|
||||
state.errors.amount = 'Amount is not a number';
|
||||
} else {
|
||||
const isToken: boolean = state.currency !== state.networkSymbol;
|
||||
const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken);
|
||||
|
||||
if (isToken) {
|
||||
const token = findToken(tokens, account.address, state.currency, account.deviceState);
|
||||
if (!token) return state;
|
||||
const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0));
|
||||
|
||||
if (!state.amount.match(decimalRegExp)) {
|
||||
state.errors.amount = `Maximum ${token.decimals} decimals allowed`;
|
||||
} else if (new BigNumber(state.total).greaterThan(account.balance)) {
|
||||
state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`;
|
||||
} else if (new BigNumber(state.amount).greaterThan(new BigNumber(token.balance).minus(pendingAmount))) {
|
||||
state.errors.amount = 'Not enough funds';
|
||||
} else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) {
|
||||
state.errors.amount = 'Amount is too low';
|
||||
}
|
||||
} else if (!state.amount.match(ETH_18_RE)) {
|
||||
state.errors.amount = 'Maximum 18 decimals allowed';
|
||||
} else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) {
|
||||
state.errors.amount = 'Not enough funds';
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Gas limit value validation
|
||||
*/
|
||||
export const gasLimitValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.gasLimit) return state;
|
||||
|
||||
const {
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
if (!network) return state;
|
||||
|
||||
const { gasLimit } = state;
|
||||
if (gasLimit.length < 1) {
|
||||
state.errors.gasLimit = 'Gas limit is not set';
|
||||
} else if (gasLimit.length > 0 && !gasLimit.match(NUMBER_RE)) {
|
||||
state.errors.gasLimit = 'Gas limit is not a number';
|
||||
} else {
|
||||
const gl: BigNumber = new BigNumber(gasLimit);
|
||||
if (gl.lessThan(1)) {
|
||||
state.errors.gasLimit = 'Gas limit is too low';
|
||||
} else if (gl.lessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) {
|
||||
state.warnings.gasLimit = 'Gas limit is below recommended';
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Gas price value validation
|
||||
*/
|
||||
export const gasPriceValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.gasPrice) return state;
|
||||
|
||||
const { gasPrice } = state;
|
||||
if (gasPrice.length < 1) {
|
||||
state.errors.gasPrice = 'Gas price is not set';
|
||||
} else if (gasPrice.length > 0 && !gasPrice.match(NUMBER_RE)) {
|
||||
state.errors.gasPrice = 'Gas price is not a number';
|
||||
} else {
|
||||
const gp: BigNumber = new BigNumber(gasPrice);
|
||||
if (gp.greaterThan(1000)) {
|
||||
state.warnings.gasPrice = 'Gas price is too high';
|
||||
} else if (gp.lessThanOrEqualTo('0')) {
|
||||
state.errors.gasPrice = 'Gas price is too low';
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Nonce value validation
|
||||
*/
|
||||
export const nonceValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.nonce) return state;
|
||||
|
||||
const {
|
||||
account,
|
||||
} = getState().selectedAccount;
|
||||
if (!account) return state;
|
||||
|
||||
const { nonce } = state;
|
||||
if (nonce.length < 1) {
|
||||
state.errors.nonce = 'Nonce is not set';
|
||||
} else if (!nonce.match(ABS_RE)) {
|
||||
state.errors.nonce = 'Nonce is not a valid number';
|
||||
} else {
|
||||
const n: BigNumber = new BigNumber(nonce);
|
||||
if (n.lessThan(account.nonce)) {
|
||||
state.warnings.nonce = 'Nonce is lower than recommended';
|
||||
} else if (n.greaterThan(account.nonce)) {
|
||||
state.warnings.nonce = 'Nonce is greater than recommended';
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Gas price value validation
|
||||
*/
|
||||
export const dataValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.data || state.data.length === 0) return state;
|
||||
if (!HEX_RE.test(state.data)) {
|
||||
state.errors.data = 'Data is not valid hexadecimal';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* UTILITIES
|
||||
*/
|
||||
|
||||
export const calculateFee = (gasPrice: string, gasLimit: string): string => {
|
||||
try {
|
||||
return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether');
|
||||
} catch (error) {
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => {
|
||||
try {
|
||||
return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10);
|
||||
} catch (error) {
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => {
|
||||
try {
|
||||
// TODO - minus pendings
|
||||
const fee = calculateFee(gasPrice, gasLimit);
|
||||
const max = balance.minus(fee);
|
||||
if (max.lessThan(0)) return '0';
|
||||
return max.toString(10);
|
||||
} catch (error) {
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array<FeeLevel> => {
|
||||
const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice;
|
||||
const quarter: BigNumber = price.dividedBy(4);
|
||||
const high: string = price.plus(quarter.times(2)).toString(10);
|
||||
const low: string = price.minus(quarter.times(2)).toString(10);
|
||||
|
||||
const customLevel: FeeLevel = selected && selected.value === 'Custom' ? {
|
||||
value: 'Custom',
|
||||
gasPrice: selected.gasPrice,
|
||||
// label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }`
|
||||
label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`,
|
||||
} : {
|
||||
value: 'Custom',
|
||||
gasPrice: low,
|
||||
label: '',
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
value: 'High',
|
||||
gasPrice: high,
|
||||
label: `${calculateFee(high, gasLimit)} ${symbol}`,
|
||||
},
|
||||
{
|
||||
value: 'Normal',
|
||||
gasPrice: gasPrice.toString(),
|
||||
label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`,
|
||||
},
|
||||
{
|
||||
value: 'Low',
|
||||
gasPrice: low,
|
||||
label: `${calculateFee(low, gasLimit)} ${symbol}`,
|
||||
},
|
||||
customLevel,
|
||||
];
|
||||
};
|
||||
|
||||
export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => {
|
||||
const { value } = selected;
|
||||
let selectedFeeLevel: ?FeeLevel;
|
||||
selectedFeeLevel = feeLevels.find(f => f.value === value);
|
||||
if (!selectedFeeLevel) {
|
||||
// fallback to default
|
||||
selectedFeeLevel = feeLevels.find(f => f.value === 'Normal');
|
||||
}
|
||||
return selectedFeeLevel || selected;
|
||||
};
|
@ -4,47 +4,56 @@
|
||||
import type { State as SendFormState } from 'reducers/SendFormReducer';
|
||||
import type {
|
||||
ThunkAction,
|
||||
PayloadAction,
|
||||
GetState,
|
||||
Dispatch,
|
||||
} from 'flowtype';
|
||||
|
||||
const PREFIX: string = 'trezor:draft-tx:';
|
||||
const TX_PREFIX: string = 'trezor:draft-tx:';
|
||||
|
||||
export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
if (typeof window.localStorage === 'undefined') return;
|
||||
|
||||
const location = getState().router.location.pathname;
|
||||
const state = getState().sendForm;
|
||||
if (!state.untouched) {
|
||||
try {
|
||||
window.sessionStorage.setItem(`${PREFIX}${location}`, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.error(`Saving sessionStorage error: ${error}`);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const load = (location: string): ?SendFormState => {
|
||||
if (typeof window.localStorage === 'undefined') return;
|
||||
export const loadDraftTransaction = (): PayloadAction<?SendFormState> => (dispatch: Dispatch, getState: GetState): ?SendFormState => {
|
||||
if (typeof window.localStorage === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const value: string = window.sessionStorage.getItem(`${PREFIX}${location}`);
|
||||
const location = getState().router.location.pathname;
|
||||
const value: string = window.sessionStorage.getItem(`${TX_PREFIX}${location}`);
|
||||
const state: ?SendFormState = JSON.parse(value);
|
||||
if (state && state.address === '' && (state.amount === '' || state.amount === '0')) {
|
||||
window.sessionStorage.removeItem(`${PREFIX}${location}`);
|
||||
return;
|
||||
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;
|
||||
}
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.error(`Loading sessionStorage error: ${error}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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(`${PREFIX}${location}`);
|
||||
window.sessionStorage.removeItem(`${TX_PREFIX}${location}`);
|
||||
} catch (error) {
|
||||
console.error(`Clearing sessionStorage error: ${error}`);
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ export type TokenAction = {
|
||||
|
||||
|
||||
// action from component <reactSelect>
|
||||
export const load = (input: string, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<any> => {
|
||||
export const load = ($input: string, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<any> => {
|
||||
let input = $input;
|
||||
if (input.length < 1) input = '0x';
|
||||
|
||||
const tokens = getState().localStorage.tokens[network];
|
||||
@ -43,10 +44,12 @@ export const load = (input: string, network: string): AsyncAction => async (disp
|
||||
return result.slice(0, 100);
|
||||
}
|
||||
|
||||
const info = await dispatch( BlockchainActions.getTokenInfo(input, network) );
|
||||
const info = await dispatch(BlockchainActions.getTokenInfo(input, network));
|
||||
if (info) {
|
||||
return [info];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const setBalance = (tokenAddress: string, ethAddress: string, balance: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
@ -63,7 +66,7 @@ export const setBalance = (tokenAddress: string, ethAddress: string, balance: st
|
||||
});
|
||||
};
|
||||
|
||||
export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
||||
const tkn: Token = {
|
||||
loaded: false,
|
||||
deviceState: account.deviceState,
|
||||
@ -81,7 +84,7 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async
|
||||
payload: tkn,
|
||||
});
|
||||
|
||||
const tokenBalance = await dispatch( BlockchainActions.getTokenBalance(tkn) );
|
||||
const tokenBalance = await dispatch(BlockchainActions.getTokenBalance(tkn));
|
||||
dispatch(setBalance(token.address, account.address, tokenBalance));
|
||||
};
|
||||
|
||||
|
@ -3,19 +3,15 @@
|
||||
import EthereumjsTx from 'ethereumjs-tx';
|
||||
import EthereumjsUnits from 'ethereumjs-units';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { toHex } from 'web3-utils';
|
||||
import { initWeb3 } from './Web3Actions';
|
||||
import { toHex } from 'web3-utils'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import { initWeb3 } from 'actions/Web3Actions';
|
||||
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
PromiseAction,
|
||||
} from 'flowtype';
|
||||
|
||||
import type {
|
||||
EthereumTransaction
|
||||
} from 'trezor-connect';
|
||||
|
||||
import type { EthereumTransaction } from 'trezor-connect';
|
||||
import type { Token } from 'reducers/TokensReducer';
|
||||
|
||||
type EthereumTxRequest = {
|
||||
@ -30,19 +26,18 @@ type EthereumTxRequest = {
|
||||
nonce: number;
|
||||
}
|
||||
|
||||
|
||||
export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<EthereumTransaction> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumTransaction> => {
|
||||
const instance = await dispatch( initWeb3(tx.network) );
|
||||
const token = tx.token;
|
||||
export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<EthereumTransaction> => async (dispatch: Dispatch): Promise<EthereumTransaction> => {
|
||||
const instance = await dispatch(initWeb3(tx.network));
|
||||
const { token } = tx;
|
||||
let data: string = `0x${tx.data}`; // TODO: check if already prefixed
|
||||
let value: string = toHex( EthereumjsUnits.convert(tx.amount, 'ether', 'wei') );
|
||||
let to: string = tx.to;
|
||||
let value: string = toHex(EthereumjsUnits.convert(tx.amount, 'ether', 'wei'));
|
||||
let to: string = tx.to; // eslint-disable-line prefer-destructuring
|
||||
|
||||
if (token) {
|
||||
// smart contract transaction
|
||||
const contract = instance.erc20.clone();
|
||||
contract.options.address = token.address;
|
||||
const tokenAmount: string = new BigNumber(tx.amount).times(Math.pow(10, token.decimals)).toString(10);
|
||||
const tokenAmount: string = new BigNumber(tx.amount).times(10 ** token.decimals).toString(10);
|
||||
data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI();
|
||||
value = '0x00';
|
||||
to = token.address;
|
||||
@ -55,15 +50,14 @@ export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<Ethereum
|
||||
chainId: instance.chainId,
|
||||
nonce: toHex(tx.nonce),
|
||||
gasLimit: toHex(tx.gasLimit),
|
||||
gasPrice: toHex( EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei') ),
|
||||
gasPrice: toHex(EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei')),
|
||||
r: '',
|
||||
s: '',
|
||||
v: '',
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const serializeEthereumTx = (tx: EthereumTransaction): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||
export const serializeEthereumTx = (tx: EthereumTransaction): PromiseAction<string> => async (): Promise<string> => {
|
||||
const ethTx = new EthereumjsTx(tx);
|
||||
return `0x${ ethTx.serialize().toString('hex') }`;
|
||||
// return toHex( ethTx.serialize() );
|
||||
}
|
||||
return `0x${ethTx.serialize().toString('hex')}`;
|
||||
};
|
@ -6,10 +6,6 @@ import * as WALLET from 'actions/constants/wallet';
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
|
||||
import type {
|
||||
Account,
|
||||
Coin,
|
||||
Discovery,
|
||||
Token,
|
||||
Device,
|
||||
TrezorDevice,
|
||||
RouterLocationState,
|
||||
|
@ -1,15 +1,10 @@
|
||||
/* @flow */
|
||||
import Web3 from 'web3';
|
||||
import HDKey from 'hdkey';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import EthereumjsUtil from 'ethereumjs-util';
|
||||
import EthereumjsUnits from 'ethereumjs-units';
|
||||
import EthereumjsTx from 'ethereumjs-tx';
|
||||
// import InputDataDecoder from 'ethereum-input-data-decoder';
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import type { EstimateGasOptions, TransactionStatus, TransactionReceipt } from 'web3';
|
||||
import { strip } from 'utils/ethUtils';
|
||||
import type { EstimateGasOptions } from 'web3';
|
||||
import * as WEB3 from 'actions/constants/web3';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
|
||||
@ -17,13 +12,11 @@ import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
ThunkAction,
|
||||
AsyncAction,
|
||||
PromiseAction,
|
||||
} from 'flowtype';
|
||||
|
||||
import type { EthereumAccount } from 'trezor-connect';
|
||||
import type { Account } from 'reducers/AccountsReducer';
|
||||
import type { PendingTx } from 'reducers/PendingTxReducer';
|
||||
import type { Web3Instance } from 'reducers/Web3Reducer';
|
||||
import type { Token } from 'reducers/TokensReducer';
|
||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||
@ -52,101 +45,97 @@ export type Web3Action = {
|
||||
} | Web3UpdateBlockAction
|
||||
| Web3UpdateGasPriceAction;
|
||||
|
||||
export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// check if requested web was initialized before
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
if (instance && instance.web3.currentProvider.connected) {
|
||||
resolve(instance);
|
||||
return;
|
||||
}
|
||||
export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => new Promise(async (resolve, reject) => {
|
||||
// check if requested web was initialized before
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
if (instance && instance.web3.currentProvider.connected) {
|
||||
resolve(instance);
|
||||
return;
|
||||
}
|
||||
|
||||
// requested web3 wasn't initialized or is disconnected
|
||||
// initialize again
|
||||
const { config, ERC20Abi } = getState().localStorage;
|
||||
const coin = config.coins.find(c => c.network === network);
|
||||
if (!coin) {
|
||||
// coin not found
|
||||
reject(new Error(`Network ${ network} not found in application config.`));
|
||||
return;
|
||||
}
|
||||
// requested web3 wasn't initialized or is disconnected
|
||||
// initialize again
|
||||
const { config, ERC20Abi } = getState().localStorage;
|
||||
const coin = config.coins.find(c => c.network === network);
|
||||
if (!coin) {
|
||||
// coin not found
|
||||
reject(new Error(`Network ${network} not found in application config.`));
|
||||
return;
|
||||
}
|
||||
|
||||
// get first url
|
||||
const url = coin.web3[ urlIndex ];
|
||||
if (!url) {
|
||||
reject(new Error('Web3 backend is not responding'));
|
||||
return;
|
||||
}
|
||||
// get first url
|
||||
const url = coin.web3[urlIndex];
|
||||
if (!url) {
|
||||
reject(new Error('Web3 backend is not responding'));
|
||||
return;
|
||||
}
|
||||
|
||||
const web3 = new Web3( new Web3.providers.WebsocketProvider(url) );
|
||||
const web3 = new Web3(new Web3.providers.WebsocketProvider(url));
|
||||
|
||||
const onConnect = async () => {
|
||||
const onConnect = async () => {
|
||||
const latestBlock = await web3.eth.getBlockNumber();
|
||||
const gasPrice = await web3.eth.getGasPrice();
|
||||
|
||||
const latestBlock = await web3.eth.getBlockNumber();
|
||||
const gasPrice = await web3.eth.getGasPrice();
|
||||
const newInstance = {
|
||||
network,
|
||||
web3,
|
||||
chainId: coin.chainId,
|
||||
erc20: new web3.eth.Contract(ERC20Abi),
|
||||
latestBlock,
|
||||
gasPrice,
|
||||
};
|
||||
|
||||
const instance = {
|
||||
network,
|
||||
web3,
|
||||
chainId: coin.chainId,
|
||||
erc20: new web3.eth.Contract(ERC20Abi),
|
||||
latestBlock,
|
||||
gasPrice,
|
||||
}
|
||||
dispatch({
|
||||
type: WEB3.CREATE,
|
||||
instance: newInstance,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: WEB3.CREATE,
|
||||
instance,
|
||||
});
|
||||
resolve(newInstance);
|
||||
};
|
||||
|
||||
resolve(instance);
|
||||
}
|
||||
const onEnd = async () => {
|
||||
web3.currentProvider.reset();
|
||||
const oldInstance = getState().web3.find(w3 => w3.network === network);
|
||||
|
||||
const onEnd = async () => {
|
||||
|
||||
web3.currentProvider.reset();
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
|
||||
if (instance && instance.web3.currentProvider.connected) {
|
||||
// backend disconnects
|
||||
// dispatch({
|
||||
// type: 'WEB3.DISCONNECT',
|
||||
// network
|
||||
// });
|
||||
} else {
|
||||
// backend initialization error for given url, try next one
|
||||
try {
|
||||
const web3 = await dispatch( initWeb3(network, urlIndex + 1) );
|
||||
resolve(web3);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
if (oldInstance && oldInstance.web3.currentProvider.connected) {
|
||||
// backend disconnects
|
||||
// dispatch({
|
||||
// type: 'WEB3.DISCONNECT',
|
||||
// network
|
||||
// });
|
||||
} else {
|
||||
// backend initialization error for given url, try next one
|
||||
try {
|
||||
const otherWeb3 = await dispatch(initWeb3(network, urlIndex + 1));
|
||||
resolve(otherWeb3);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
web3.currentProvider.on('connect', onConnect);
|
||||
web3.currentProvider.on('end', onEnd);
|
||||
web3.currentProvider.on('error', onEnd);
|
||||
});
|
||||
}
|
||||
web3.currentProvider.on('connect', onConnect);
|
||||
web3.currentProvider.on('end', onEnd);
|
||||
web3.currentProvider.on('error', onEnd);
|
||||
});
|
||||
|
||||
export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumAccount> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const balance = await instance.web3.eth.getBalance(address);
|
||||
const nonce = await instance.web3.eth.getTransactionCount(address);
|
||||
return {
|
||||
return {
|
||||
address,
|
||||
transactions: 0,
|
||||
block: 0,
|
||||
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
|
||||
nonce
|
||||
nonce,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const pending = getState().pending.filter(p => p.network === network);
|
||||
for (const tx of pending) {
|
||||
pending.forEach(async (tx) => {
|
||||
const status = await instance.web3.eth.getTransaction(tx.id);
|
||||
if (!status) {
|
||||
dispatch({
|
||||
@ -169,14 +158,15 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getPendingInfo = (network: string, txid: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
/*
|
||||
export const getPendingInfo = (network: string, txid: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const tx = await instance.web3.eth.getTransaction(txid);
|
||||
|
||||
/*
|
||||
|
||||
if (tx.input !== "0x") {
|
||||
// find token:
|
||||
// tx.to <= smart contract address
|
||||
@ -187,34 +177,36 @@ export const getPendingInfo = (network: string, txid: string): PromiseAction<voi
|
||||
if (data.name === 'transfer') {
|
||||
console.warn("DATA!", data.inputs[0], data.inputs[1].toString(10));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
*/
|
||||
// return tx;
|
||||
}
|
||||
|
||||
export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3("ropsten") );
|
||||
// return tx;
|
||||
};
|
||||
|
||||
export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch(initWeb3('ropsten'));
|
||||
// const inputData = instance.web3.utils.hexToAscii("0xa9059cbb00000000000000000000000073d0385f4d8e00c5e6504c6030f47bf6212736a80000000000000000000000000000000000000000000000000000000000000001");
|
||||
// console.warn("input data!", inputData);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
|
||||
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const balance = await instance.web3.eth.getBalance(account.address);
|
||||
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
||||
dispatch( AccountsActions.update( { ...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce }) );
|
||||
dispatch(AccountsActions.update({
|
||||
...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce,
|
||||
}));
|
||||
|
||||
// update tokens for this account
|
||||
dispatch( updateAccountTokens(account) );
|
||||
}
|
||||
dispatch(updateAccountTokens(account));
|
||||
};
|
||||
|
||||
export const updateAccountTokens = (account: Account): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.address);
|
||||
for (const token of tokens) {
|
||||
const balance = await dispatch( getTokenBalance(token) );
|
||||
tokens.forEach(async (token) => {
|
||||
const balance = await dispatch(getTokenBalance(token));
|
||||
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
|
||||
if (balance !== token.balance) {
|
||||
dispatch(TokenActions.setBalance(
|
||||
@ -223,11 +215,11 @@ export const updateAccountTokens = (account: Account): PromiseAction<void> => as
|
||||
balance,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => {
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const contract = instance.erc20.clone();
|
||||
contract.options.address = address;
|
||||
|
||||
@ -243,68 +235,71 @@ export const getTokenInfo = (address: string, network: string): PromiseAction<Ne
|
||||
};
|
||||
};
|
||||
|
||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||
const instance = await dispatch( initWeb3(token.network) );
|
||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||
const instance = await dispatch(initWeb3(token.network));
|
||||
const contract = instance.erc20.clone();
|
||||
contract.options.address = token.address;
|
||||
|
||||
const balance = await contract.methods.balanceOf(token.ethAddress).call();
|
||||
return new BigNumber(balance).dividedBy(Math.pow(10, token.decimals)).toString(10);
|
||||
return new BigNumber(balance).dividedBy(10 ** token.decimals).toString(10);
|
||||
};
|
||||
|
||||
export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
if (instance) {
|
||||
return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei');
|
||||
} else {
|
||||
throw "0";
|
||||
}
|
||||
}
|
||||
return '0';
|
||||
};
|
||||
|
||||
export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const instance = await dispatch( initWeb3(network) );
|
||||
const instance = await dispatch(initWeb3(network));
|
||||
const gasPrice = await instance.web3.eth.getGasPrice();
|
||||
if (instance.gasPrice !== gasPrice) {
|
||||
dispatch({
|
||||
type: WEB3.GAS_PRICE_UPDATED,
|
||||
network,
|
||||
gasPrice
|
||||
gasPrice,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// silent action
|
||||
// nothing happens if this fails
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const estimateGasLimit = (network: string, options: EstimateGasOptions): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
|
||||
const instance = await dispatch( initWeb3(network) );
|
||||
export const estimateGasLimit = (network: string, $options: EstimateGasOptions): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||
const instance = await dispatch(initWeb3(network));
|
||||
// TODO: allow data starting with 0x ...
|
||||
options.to = '0x0000000000000000000000000000000000000000';
|
||||
options.data = `0x${options.data.length % 2 === 0 ? options.data : `0${options.data}`}`;
|
||||
options.value = instance.web3.utils.toHex( EthereumjsUnits.convert(options.value || '0', 'ether', 'wei') );
|
||||
options.gasPrice = instance.web3.utils.toHex( EthereumjsUnits.convert(options.gasPrice, 'gwei', 'wei') );
|
||||
const data = `0x${$options.data.length % 2 === 0 ? $options.data : `0${$options.data}`}`;
|
||||
const options = {
|
||||
...$options,
|
||||
to: '0x0000000000000000000000000000000000000000',
|
||||
data,
|
||||
value: instance.web3.utils.toHex(EthereumjsUnits.convert($options.value || '0', 'ether', 'wei')),
|
||||
gasPrice: instance.web3.utils.toHex(EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei')),
|
||||
};
|
||||
|
||||
const limit = await instance.web3.eth.estimateGas(options);
|
||||
return limit;
|
||||
return limit.toString();
|
||||
};
|
||||
|
||||
export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
||||
const network: string = coinInfo.shortcut.toLowerCase();
|
||||
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
||||
const network: string = coinInfo.shortcut.toLowerCase();
|
||||
// check if Web3 was already initialized
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
if (instance) {
|
||||
// reset current connection
|
||||
instance.web3.currentProvider.reset();
|
||||
instance.web3.currentProvider.connection.close();
|
||||
|
||||
|
||||
// remove instance from reducer
|
||||
dispatch({
|
||||
type: WEB3.DISCONNECT,
|
||||
instance
|
||||
instance,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,23 +1,9 @@
|
||||
/* @flow */
|
||||
|
||||
|
||||
export const INIT: 'send__init' = 'send__init';
|
||||
export const DISPOSE: 'send__dispose' = 'send__dispose';
|
||||
export const CHANGE: 'send__change' = 'send__change';
|
||||
export const VALIDATION: 'send__validation' = 'send__validation';
|
||||
export const ADDRESS_VALIDATION: 'send__address_validation' = 'send__address_validation';
|
||||
export const ADDRESS_CHANGE: 'send__address_change' = 'send__address_change';
|
||||
export const AMOUNT_CHANGE: 'send__amount_change' = 'send__amount_change';
|
||||
export const SET_MAX: 'send__set_max' = 'send__set_max';
|
||||
export const CURRENCY_CHANGE: 'send__currency_change' = 'send__currency_change';
|
||||
export const FEE_LEVEL_CHANGE: 'send__fee_level_change' = 'send__fee_level_change';
|
||||
export const GAS_PRICE_CHANGE: 'send__gas_price_change' = 'send__gas_price_change';
|
||||
export const GAS_LIMIT_CHANGE: 'send__gas_limit_change' = 'send__gas_limit_change';
|
||||
export const NONCE_CHANGE: 'send__nonce_change' = 'send__nonce_change';
|
||||
export const UPDATE_FEE_LEVELS: 'send__update_fee_levels' = 'send__update_fee_levels';
|
||||
export const DATA_CHANGE: 'send__data_change' = 'send__data_change';
|
||||
export const SEND: 'send__submit' = 'send__submit';
|
||||
export const TX_SENDING: 'send__tx_sending' = 'send__tx_sending';
|
||||
export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete';
|
||||
export const TX_ERROR: 'send__tx_error' = 'send__tx_error';
|
||||
export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced';
|
||||
|
||||
export const FROM_SESSION_STORAGE: 'send__from_session_storage' = 'send__from_session_storage';
|
@ -56,7 +56,7 @@ const Name = styled.div`
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: no-wrap;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${colors.TEXT_PRIMARY};
|
||||
|
@ -3,6 +3,16 @@ import PropTypes from 'prop-types';
|
||||
import colors from 'config/colors';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const chooseIconAnimationType = (canAnimate, isActive) => {
|
||||
if (canAnimate) {
|
||||
if (isActive) {
|
||||
return rotate180up;
|
||||
}
|
||||
return rotate180down;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// TODO: make animation of icons better
|
||||
const rotate180up = keyframes`
|
||||
from {
|
||||
@ -23,7 +33,7 @@ const rotate180down = keyframes`
|
||||
`;
|
||||
|
||||
const SvgWrapper = styled.svg`
|
||||
animation: ${props => (props.canAnimate ? (props.isActive ? rotate180up : rotate180down) : null)} 0.2s linear 1 forwards;
|
||||
animation: ${props => chooseIconAnimationType(props.canAnimate, props.isActive)} 0.2s linear 1 forwards;
|
||||
|
||||
:hover {
|
||||
path {
|
||||
|
@ -45,7 +45,7 @@ const createAccount = (state: State, account: Account): State => {
|
||||
if (exist) {
|
||||
return state;
|
||||
}
|
||||
const newState: State = [ ...state ];
|
||||
const newState: State = [...state];
|
||||
newState.push(account);
|
||||
return newState;
|
||||
};
|
||||
@ -66,7 +66,7 @@ const updateAccount = (state: State, account: Account): State => {
|
||||
const newState: State = [...state];
|
||||
newState[index] = account;
|
||||
return newState;
|
||||
}
|
||||
};
|
||||
|
||||
const setBalance = (state: State, action: AccountSetBalanceAction): State => {
|
||||
// const index: number = state.findIndex(account => account.address === action.address && account.network === action.network && account.deviceState === action.deviceState);
|
||||
@ -100,7 +100,7 @@ export default (state: State = initialState, action: Action): State => {
|
||||
//case CONNECT.FORGET_SINGLE :
|
||||
// return forgetAccounts(state, action);
|
||||
|
||||
case ACCOUNT.UPDATE :
|
||||
case ACCOUNT.UPDATE:
|
||||
return updateAccount(state, action.payload);
|
||||
|
||||
case ACCOUNT.SET_BALANCE:
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* @flow */
|
||||
|
||||
|
||||
import { RATE_UPDATE } from 'services/CoinmarketcapService';
|
||||
|
||||
import type { Action } from 'flowtype';
|
||||
@ -27,7 +26,6 @@ const update = (state: Array<Fiat>, action: FiatRateAction): Array<Fiat> => {
|
||||
return newState;
|
||||
};
|
||||
|
||||
|
||||
export default (state: Array<Fiat> = initialState, action: Action): Array<Fiat> => {
|
||||
switch (action.type) {
|
||||
case RATE_UPDATE:
|
||||
|
@ -94,7 +94,6 @@ export default function modal(state: State = initialState, action: Action): Stat
|
||||
|
||||
case UI.CLOSE_UI_WINDOW:
|
||||
case MODAL.CLOSE:
|
||||
|
||||
case CONNECT.FORGET:
|
||||
case CONNECT.FORGET_SINGLE:
|
||||
case CONNECT.REMEMBER:
|
||||
|
@ -39,11 +39,13 @@ const add = (state: State, action: SendTxAction): State => {
|
||||
return newState;
|
||||
};
|
||||
|
||||
const add_NEW = (state: State, payload: any): State => {
|
||||
/*
|
||||
const addFromBloockbokNotifiaction = (state: State, payload: any): State => {
|
||||
const newState = [...state];
|
||||
newState.push(payload);
|
||||
return newState;
|
||||
};
|
||||
*/
|
||||
|
||||
const remove = (state: State, id: string): State => state.filter(tx => tx.id !== id);
|
||||
|
||||
|
@ -1,17 +1,9 @@
|
||||
/* @flow */
|
||||
|
||||
|
||||
import EthereumjsUnits from 'ethereumjs-units';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as WEB3 from 'actions/constants/web3';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
|
||||
import { getFeeLevels } from 'actions/SendFormActions';
|
||||
|
||||
import type { Action } from 'flowtype';
|
||||
import type {
|
||||
Web3UpdateGasPriceAction,
|
||||
} from 'actions/Web3Actions';
|
||||
|
||||
export type FeeLevel = {
|
||||
label: string;
|
||||
@ -82,46 +74,16 @@ export const initialState: State = {
|
||||
infos: {},
|
||||
};
|
||||
|
||||
|
||||
const onGasPriceUpdated = (state: State, action: Web3UpdateGasPriceAction): State => {
|
||||
// function getRandomInt(min, max) {
|
||||
// return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
// }
|
||||
// const newPrice = getRandomInt(10, 50).toString();
|
||||
const newPrice: string = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei');
|
||||
if (action.network === state.networkName && newPrice !== state.recommendedGasPrice) {
|
||||
const newState: State = { ...state };
|
||||
if (!state.untouched) {
|
||||
newState.gasPriceNeedsUpdate = true;
|
||||
newState.recommendedGasPrice = newPrice;
|
||||
} else {
|
||||
const newFeeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit);
|
||||
const selectedFeeLevel: ?FeeLevel = newFeeLevels.find(f => f.value === 'Normal');
|
||||
if (!selectedFeeLevel) return state;
|
||||
newState.recommendedGasPrice = newPrice;
|
||||
newState.feeLevels = newFeeLevels;
|
||||
newState.selectedFeeLevel = selectedFeeLevel;
|
||||
newState.gasPrice = selectedFeeLevel.gasPrice;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
export default (state: State = initialState, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case SEND.INIT:
|
||||
case SEND.CHANGE:
|
||||
case SEND.VALIDATION:
|
||||
return action.state;
|
||||
|
||||
case ACCOUNT.DISPOSE:
|
||||
return initialState;
|
||||
|
||||
// this will be called right after Web3 instance initialization before any view is shown
|
||||
// and async during app live time
|
||||
case WEB3.GAS_PRICE_UPDATED:
|
||||
return onGasPriceUpdated(state, action);
|
||||
|
||||
|
||||
case SEND.TOGGLE_ADVANCED:
|
||||
return {
|
||||
@ -129,22 +91,7 @@ export default (state: State = initialState, action: Action): State => {
|
||||
advanced: !state.advanced,
|
||||
};
|
||||
|
||||
|
||||
// user actions
|
||||
case SEND.ADDRESS_CHANGE:
|
||||
case SEND.ADDRESS_VALIDATION:
|
||||
case SEND.AMOUNT_CHANGE:
|
||||
case SEND.SET_MAX:
|
||||
case SEND.CURRENCY_CHANGE:
|
||||
case SEND.FEE_LEVEL_CHANGE:
|
||||
case SEND.UPDATE_FEE_LEVELS:
|
||||
case SEND.GAS_PRICE_CHANGE:
|
||||
case SEND.GAS_LIMIT_CHANGE:
|
||||
case SEND.NONCE_CHANGE:
|
||||
case SEND.DATA_CHANGE:
|
||||
return action.state;
|
||||
|
||||
case SEND.SEND:
|
||||
case SEND.TX_SENDING:
|
||||
return {
|
||||
...state,
|
||||
sending: true,
|
||||
@ -156,32 +103,6 @@ export default (state: State = initialState, action: Action): State => {
|
||||
sending: false,
|
||||
};
|
||||
|
||||
case SEND.VALIDATION:
|
||||
return {
|
||||
...state,
|
||||
errors: action.errors,
|
||||
warnings: action.warnings,
|
||||
infos: action.infos,
|
||||
};
|
||||
|
||||
case SEND.FROM_SESSION_STORAGE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
address: action.address,
|
||||
amount: action.amount,
|
||||
setMax: action.setMax,
|
||||
selectedCurrency: action.selectedCurrency,
|
||||
selectedFeeLevel: action.selectedFeeLevel,
|
||||
advanced: action.advanced,
|
||||
gasLimit: action.gasLimit,
|
||||
gasPrice: action.gasPrice,
|
||||
data: action.data,
|
||||
nonce: action.nonce,
|
||||
untouched: false,
|
||||
touched: action.touched,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
import Web3 from 'web3';
|
||||
|
||||
import type { Contract } from 'web3';
|
||||
import * as STORAGE from 'actions/constants/localStorage';
|
||||
import * as WEB3 from 'actions/constants/web3';
|
||||
|
||||
import type { Action } from 'flowtype';
|
||||
|
@ -40,7 +40,7 @@ const reducers = {
|
||||
fiat,
|
||||
wallet,
|
||||
devices,
|
||||
blockchain
|
||||
blockchain,
|
||||
};
|
||||
|
||||
export type Reducers = typeof reducers;
|
||||
|
@ -115,3 +115,22 @@ export const getWeb3 = (state: State): ?Web3Instance => {
|
||||
if (!locationState.network) return null;
|
||||
return state.web3.find(w3 => w3.network === locationState.network);
|
||||
};
|
||||
|
||||
export const observeChanges = (prev: ?(Object | Array<any>), current: ?(Object | Array<any>), fields?: Array<string>): boolean => {
|
||||
if (prev !== current) {
|
||||
// 1. one of the objects is null/undefined
|
||||
if (!prev || !current) return true;
|
||||
// 2. object are Arrays and they have different length
|
||||
if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length;
|
||||
// 3. no nested field to check
|
||||
if (!Array.isArray(fields)) return true;
|
||||
// 4. validate nested field
|
||||
if (prev instanceof Object && current instanceof Object) {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const key = fields[i];
|
||||
if (prev[key] !== current[key]) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
@ -13,7 +13,6 @@ import type {
|
||||
AsyncAction,
|
||||
GetState,
|
||||
} from 'flowtype';
|
||||
import type { Config, FiatValueTicker } from 'reducers/LocalStorageReducer';
|
||||
|
||||
export const RATE_UPDATE: 'rate__update' = 'rate__update';
|
||||
|
||||
@ -24,11 +23,11 @@ export type FiatRateAction = {
|
||||
}
|
||||
|
||||
const loadRateAction = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const config: ?Config = getState().localStorage.config;
|
||||
const { config } = getState().localStorage;
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
config.fiatValueTickers.forEach(async (ticker: FiatValueTicker) => {
|
||||
config.fiatValueTickers.forEach(async (ticker) => {
|
||||
// const rate: ?Array<any> = await JSONRequest(`${ticker.url}?convert=USD`, 'json');
|
||||
const rate: ?Array<any> = await httpRequest(`${ticker.url}?convert=USD`, 'json');
|
||||
if (rate) {
|
||||
|
@ -10,6 +10,7 @@ import * as NotificationActions from 'actions/NotificationActions';
|
||||
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
||||
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
||||
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
|
||||
import * as SendFormActionActions from 'actions/SendFormActions';
|
||||
|
||||
import type {
|
||||
Middleware,
|
||||
@ -91,6 +92,8 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
||||
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
|
||||
}
|
||||
|
||||
// observe send form props changes
|
||||
api.dispatch(SendFormActionActions.observe(prevState, action));
|
||||
|
||||
// update common values in WallerReducer
|
||||
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
|
||||
|
21
src/store.js
21
src/store.js
@ -3,8 +3,6 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
// import createHistory from 'history/createBrowserHistory';
|
||||
// import { useRouterHistory } from 'react-router';
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import reducers from 'reducers';
|
||||
@ -17,17 +15,21 @@ import type { Action, GetState, Store } from 'flowtype';
|
||||
|
||||
export const history: History = createHistory({ queryKey: false });
|
||||
|
||||
const RAVEN_KEY: string = 'https://497392c3ff6e46dc9e54eef123979378@sentry.io/294339';
|
||||
Raven.config(RAVEN_KEY).install();
|
||||
|
||||
const initialState: any = {};
|
||||
const enhancers = [];
|
||||
const middleware = [
|
||||
|
||||
const middlewares = [
|
||||
thunk,
|
||||
RavenMiddleware(RAVEN_KEY),
|
||||
routerMiddleware(history),
|
||||
];
|
||||
|
||||
// sentry io middleware only in dev build
|
||||
if (process.env.BUILD === 'development') {
|
||||
const RAVEN_KEY = 'https://34b8c09deb6c4cd2a4dc3f0029cd02d8@sentry.io/1279550';
|
||||
const ravenMiddleware = RavenMiddleware(RAVEN_KEY);
|
||||
Raven.config(RAVEN_KEY).install();
|
||||
middlewares.push(ravenMiddleware);
|
||||
}
|
||||
|
||||
let composedEnhancers: any;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@ -50,13 +52,12 @@ if (process.env.NODE_ENV === 'development') {
|
||||
}
|
||||
|
||||
composedEnhancers = compose(
|
||||
applyMiddleware(logger, ...middleware, ...services),
|
||||
applyMiddleware(logger, ...middlewares, ...services),
|
||||
...enhancers,
|
||||
);
|
||||
|
||||
} else {
|
||||
composedEnhancers = compose(
|
||||
applyMiddleware(...middleware, ...services),
|
||||
applyMiddleware(...middlewares, ...services),
|
||||
...enhancers,
|
||||
);
|
||||
}
|
||||
|
@ -5,14 +5,11 @@ import EthereumjsUtil from 'ethereumjs-util';
|
||||
|
||||
export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16);
|
||||
|
||||
export const padLeftEven = (hex: string): string => {
|
||||
hex = hex.length % 2 != 0 ? `0${hex}` : hex;
|
||||
return hex;
|
||||
};
|
||||
export const padLeftEven = (hex: string): string => (hex.length % 2 !== 0 ? `0${hex}` : hex);
|
||||
|
||||
export const sanitizeHex = (hex: number | string): ?string => {
|
||||
if (typeof hex !== 'string') return null;
|
||||
hex = hex.substring(0, 2) === '0x' ? hex.substring(2) : hex;
|
||||
export const sanitizeHex = ($hex: number | string): ?string => {
|
||||
if (typeof $hex !== 'string') return null;
|
||||
const hex = $hex.substring(0, 2) === '0x' ? $hex.substring(2) : $hex;
|
||||
if (hex === '') return '';
|
||||
return `0x${padLeftEven(hex)}`;
|
||||
};
|
||||
@ -35,10 +32,12 @@ export const validateAddress = (address: string): ?string => {
|
||||
const hasUpperCase = new RegExp('^(.*[A-Z].*)$');
|
||||
if (address.length < 1) {
|
||||
return 'Address is not set';
|
||||
} else if (!EthereumjsUtil.isValidAddress(address)) {
|
||||
}
|
||||
if (!EthereumjsUtil.isValidAddress(address)) {
|
||||
return 'Address is not valid';
|
||||
} else if (address.match(hasUpperCase) && !EthereumjsUtil.isValidChecksumAddress(address)) {
|
||||
}
|
||||
if (address.match(hasUpperCase) && !EthereumjsUtil.isValidChecksumAddress(address)) {
|
||||
return 'Address is not a valid checksum';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
@ -1,12 +1,5 @@
|
||||
/* @flow */
|
||||
|
||||
|
||||
// import root from 'window-or-global';
|
||||
// import Promise from 'es6-promise';
|
||||
|
||||
export async function resolveAfter(msec: number, value?: any): Promise<any> {
|
||||
await new Promise((resolve) => {
|
||||
//root.setTimeout(resolve, msec, value);
|
||||
window.setTimeout(resolve, msec, value);
|
||||
});
|
||||
}
|
||||
export const resolveAfter = (msec: number, value?: any): Promise<any> => new Promise((resolve) => {
|
||||
window.setTimeout(resolve, msec, value);
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import CaseImage from 'images/case.png';
|
||||
import styled from 'styled-components';
|
||||
import Header from 'components/Header';
|
||||
import Footer from 'components/Footer';
|
||||
@ -39,11 +39,10 @@ const LandingContent = styled.div`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const LandingImage = styled.div`
|
||||
const LandingImage = styled.img`
|
||||
width: 777px;
|
||||
min-height: 500px;
|
||||
margin: auto;
|
||||
background-image: url('../images/case.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 0px;
|
||||
background-size: contain;
|
||||
@ -120,7 +119,7 @@ export default (props: Props) => {
|
||||
showDisconnect={shouldShowDisconnectDevice}
|
||||
/>
|
||||
|
||||
<LandingImage />
|
||||
<LandingImage src={CaseImage} />
|
||||
|
||||
{shouldShowConnectDevice && (
|
||||
<LandingFooterWrapper>
|
||||
|
@ -0,0 +1,219 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
|
||||
import Input from 'components/inputs/Input';
|
||||
import Textarea from 'components/Textarea';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import ICONS from 'config/icons';
|
||||
|
||||
import type { Props } from '../../Container';
|
||||
|
||||
const InputRow = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const InputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const GreenSpan = styled.span`
|
||||
color: ${colors.GREEN_PRIMARY};
|
||||
`;
|
||||
|
||||
const AdvancedSettingsWrapper = styled.div`
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
border-top: 1px solid ${colors.DIVIDER};
|
||||
`;
|
||||
|
||||
const GasInputRow = styled(InputRow)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const GasInput = styled(Input)`
|
||||
&:first-child {
|
||||
padding-right: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTextarea = styled(Textarea)`
|
||||
margin-bottom: 20px;
|
||||
height: 80px;
|
||||
`;
|
||||
|
||||
const AdvancedSettingsSendButtonWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const getGasLimitInputState = (gasLimitErrors: string, gasLimitWarnings: string): string => {
|
||||
let state = '';
|
||||
if (gasLimitWarnings && !gasLimitErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (gasLimitErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getGasPriceInputState = (gasPriceErrors: string, gasPriceWarnings: string): string => {
|
||||
let state = '';
|
||||
if (gasPriceWarnings && !gasPriceErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (gasPriceErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
// stateless component
|
||||
const AdvancedForm = (props: Props) => {
|
||||
const {
|
||||
network,
|
||||
} = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
const {
|
||||
networkSymbol,
|
||||
currency,
|
||||
recommendedGasPrice,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
data,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
} = props.sendForm;
|
||||
const {
|
||||
onGasLimitChange,
|
||||
onGasPriceChange,
|
||||
onDataChange,
|
||||
} = props.sendFormActions;
|
||||
|
||||
let gasLimitTooltipCurrency: string;
|
||||
let gasLimitTooltipValue: string;
|
||||
if (networkSymbol !== currency) {
|
||||
gasLimitTooltipCurrency = 'tokens';
|
||||
gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10);
|
||||
} else {
|
||||
gasLimitTooltipCurrency = networkSymbol;
|
||||
gasLimitTooltipValue = network.defaultGasLimit.toString(10);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdvancedSettingsWrapper>
|
||||
<GasInputRow>
|
||||
<GasInput
|
||||
state={getGasLimitInputState(errors.gasLimit, warnings.gasLimit)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Gas limit
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Gas limit is the amount of gas to send with your transaction.<br />
|
||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||
Increasing this number will not get your TX mined faster.<br />
|
||||
Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
)}
|
||||
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit}
|
||||
value={gasLimit}
|
||||
isDisabled={networkSymbol === currency && data.length > 0}
|
||||
onChange={event => onGasLimitChange(event.target.value)}
|
||||
/>
|
||||
|
||||
<GasInput
|
||||
state={getGasPriceInputState(errors.gasPrice, warnings.gasPrice)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Gas price
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Gas Price is the amount you pay per unit of gas.<br />
|
||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||
Higher the gas price = faster transaction, but more expensive. Recommended is <GreenSpan>{recommendedGasPrice} GWEI.</GreenSpan><br />
|
||||
<Link href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener" isGreen>Read more</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
)}
|
||||
bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice}
|
||||
value={gasPrice}
|
||||
onChange={event => onGasPriceChange(event.target.value)}
|
||||
/>
|
||||
</GasInputRow>
|
||||
|
||||
<StyledTextarea
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Data
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Data is usually used when you send transactions to contracts.
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
)}
|
||||
bottomText={errors.data || warnings.data || infos.data}
|
||||
disabled={networkSymbol !== currency}
|
||||
value={networkSymbol !== currency ? '' : data}
|
||||
onChange={event => onDataChange(event.target.value)}
|
||||
/>
|
||||
|
||||
<AdvancedSettingsSendButtonWrapper>
|
||||
{ props.children }
|
||||
</AdvancedSettingsSendButtonWrapper>
|
||||
</AdvancedSettingsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedForm;
|
@ -1,6 +1,6 @@
|
||||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Select } from 'components/Select';
|
||||
import Button from 'components/Button';
|
||||
@ -12,11 +12,9 @@ import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
|
||||
import colors from 'config/colors';
|
||||
import P from 'components/Paragraph';
|
||||
import { H2 } from 'components/Heading';
|
||||
import Textarea from 'components/Textarea';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import { calculate, validation } from 'actions/SendFormActions';
|
||||
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
||||
import type { Token } from 'flowtype';
|
||||
import AdvancedForm from './components/AdvancedForm';
|
||||
import PendingTransactions from './components/PendingTransactions';
|
||||
|
||||
import type { Props } from './Container';
|
||||
@ -25,11 +23,6 @@ import type { Props } from './Container';
|
||||
// and put it inside config/variables.js
|
||||
const SmallScreenWidth = '850px';
|
||||
|
||||
type State = {
|
||||
isAdvancedSettingsHidden: boolean,
|
||||
shouldAnimateAdvancedSettingsToggle: boolean,
|
||||
};
|
||||
|
||||
const Wrapper = styled.section`
|
||||
padding: 0 48px;
|
||||
`;
|
||||
@ -38,6 +31,16 @@ const StyledH2 = styled(H2)`
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const AmountInputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const AmountInputLabel = styled.span`
|
||||
text-align: right;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const InputRow = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
@ -143,453 +146,264 @@ const SendButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
const AdvancedSettingsWrapper = styled.div`
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
border-top: 1px solid ${colors.DIVIDER};
|
||||
`;
|
||||
|
||||
const GasInputRow = styled(InputRow)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const GasInput = styled(Input)`
|
||||
&:first-child {
|
||||
padding-right: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AdvancedSettingsSendButtonWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const StyledTextarea = styled(Textarea)`
|
||||
margin-bottom: 20px;
|
||||
height: 80px;
|
||||
`;
|
||||
|
||||
const AdvancedSettingsIcon = styled(Icon)`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const GreenSpan = styled.span`
|
||||
color: ${colors.GREEN_PRIMARY};
|
||||
`;
|
||||
// render helpers
|
||||
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
|
||||
let state = '';
|
||||
if (address && !addressErrors) {
|
||||
state = 'success';
|
||||
}
|
||||
if (addressWarnings && !addressErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (addressErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const InputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
const getAmountInputState = (amountErrors: string, amountWarnings: string): string => {
|
||||
let state = '';
|
||||
if (amountWarnings && !amountErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (amountErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
class AccountSend extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAdvancedSettingsHidden: true,
|
||||
shouldAnimateAdvancedSettingsToggle: false,
|
||||
};
|
||||
const getTokensSelectData = (tokens: Array<Token>, accountNetwork: any): Array<{ value: string, label: string }> => {
|
||||
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
||||
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
||||
|
||||
return tokensSelectData;
|
||||
};
|
||||
|
||||
// stateless component
|
||||
const AccountSend = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
} = props.selectedAccount;
|
||||
const {
|
||||
address,
|
||||
amount,
|
||||
setMax,
|
||||
networkSymbol,
|
||||
currency,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
gasPriceNeedsUpdate,
|
||||
total,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
sending,
|
||||
advanced,
|
||||
} = props.sendForm;
|
||||
const {
|
||||
toggleAdvanced,
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onCurrencyChange,
|
||||
onFeeLevelChange,
|
||||
updateFeeLevels,
|
||||
onSend,
|
||||
} = props.sendFormActions;
|
||||
|
||||
const isCurrentCurrencyToken = networkSymbol !== currency;
|
||||
|
||||
let selectedTokenBalance = 0;
|
||||
const selectedToken = tokens.find(t => t.symbol === currency);
|
||||
if (selectedToken) {
|
||||
selectedTokenBalance = selectedToken.balance;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps: Props) {
|
||||
calculate(this.props, newProps);
|
||||
validation(newProps);
|
||||
if (!device || !account || !discovery || !network) return null;
|
||||
|
||||
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||
let sendButtonText: string = 'Send';
|
||||
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
||||
sendButtonText += ` ${amount} ${currency.toUpperCase()}`;
|
||||
} else if (networkSymbol === currency && total !== '0') {
|
||||
sendButtonText += ` ${total} ${network.symbol}`;
|
||||
}
|
||||
|
||||
getAddressInputState(address: string, addressErrors: string, addressWarnings: string) {
|
||||
let state = '';
|
||||
if (address && !addressErrors) {
|
||||
state = 'success';
|
||||
}
|
||||
if (addressWarnings && !addressErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (addressErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
if (!device.connected) {
|
||||
sendButtonText = 'Device is not connected';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!device.available) {
|
||||
sendButtonText = 'Device is unavailable';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!discovery.completed) {
|
||||
sendButtonText = 'Loading accounts';
|
||||
isSendButtonDisabled = true;
|
||||
}
|
||||
|
||||
getAmountInputState(amountErrors: string, amountWarnings: string) {
|
||||
let state = '';
|
||||
if (amountWarnings && !amountErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (amountErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
const tokensSelectData = getTokensSelectData(tokens, network);
|
||||
const isAdvancedSettingsHidden = !advanced;
|
||||
|
||||
getGasLimitInputState(gasLimitErrors: string, gasLimitWarnings: string) {
|
||||
let state = '';
|
||||
if (gasLimitWarnings && !gasLimitErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (gasLimitErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
return (
|
||||
<SelectedAccount {...props}>
|
||||
<Wrapper>
|
||||
<StyledH2>Send Ethereum or tokens</StyledH2>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAddressInputState(address, errors.address, warnings.address)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Address"
|
||||
bottomText={errors.address || warnings.address || infos.address}
|
||||
value={address}
|
||||
onChange={event => onAddressChange(event.target.value)}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
getGasPriceInputState(gasPriceErrors: string, gasPriceWarnings: string) {
|
||||
let state = '';
|
||||
if (gasPriceWarnings && !gasPriceErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (gasPriceErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
getTokensSelectData(tokens: Array<Token>, accountNetwork: any) {
|
||||
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
||||
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
||||
|
||||
return tokensSelectData;
|
||||
}
|
||||
|
||||
handleToggleAdvancedSettingsButton() {
|
||||
this.toggleAdvancedSettings();
|
||||
}
|
||||
|
||||
toggleAdvancedSettings() {
|
||||
this.setState(previousState => ({
|
||||
isAdvancedSettingsHidden: !previousState.isAdvancedSettingsHidden,
|
||||
shouldAnimateAdvancedSettingsToggle: true,
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const device = this.props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
} = this.props.selectedAccount;
|
||||
const {
|
||||
address,
|
||||
amount,
|
||||
setMax,
|
||||
networkSymbol,
|
||||
currency,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
recommendedGasPrice,
|
||||
gasPriceNeedsUpdate,
|
||||
total,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
data,
|
||||
sending,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
} = this.props.sendForm;
|
||||
|
||||
const {
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onCurrencyChange,
|
||||
onFeeLevelChange,
|
||||
updateFeeLevels,
|
||||
onSend,
|
||||
onGasLimitChange,
|
||||
onGasPriceChange,
|
||||
onDataChange,
|
||||
} = this.props.sendFormActions;
|
||||
|
||||
if (!device || !account || !discovery || !network) return null;
|
||||
|
||||
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||
let sendButtonText: string = 'Send';
|
||||
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
||||
sendButtonText += ` ${amount} ${currency.toUpperCase()}`;
|
||||
} else if (networkSymbol === currency && total !== '0') {
|
||||
sendButtonText += ` ${total} ${network.symbol}`;
|
||||
}
|
||||
|
||||
if (!device.connected) {
|
||||
sendButtonText = 'Device is not connected';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!device.available) {
|
||||
sendButtonText = 'Device is unavailable';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!discovery.completed) {
|
||||
sendButtonText = 'Loading accounts';
|
||||
isSendButtonDisabled = true;
|
||||
}
|
||||
|
||||
const tokensSelectData = this.getTokensSelectData(tokens, network);
|
||||
|
||||
let gasLimitTooltipCurrency: string;
|
||||
let gasLimitTooltipValue: string;
|
||||
if (networkSymbol !== currency) {
|
||||
gasLimitTooltipCurrency = 'tokens';
|
||||
gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10);
|
||||
} else {
|
||||
gasLimitTooltipCurrency = networkSymbol;
|
||||
gasLimitTooltipValue = network.defaultGasLimit.toString(10);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SelectedAccount {...this.props}>
|
||||
<Wrapper>
|
||||
<StyledH2>Send Ethereum or tokens</StyledH2>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={this.getAddressInputState(address, errors.address, warnings.address)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Address"
|
||||
bottomText={errors.address || warnings.address || infos.address}
|
||||
value={address}
|
||||
onChange={event => onAddressChange(event.target.value)}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow>
|
||||
<Input
|
||||
state={this.getAmountInputState(errors.amount, warnings.amount)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Amount"
|
||||
value={amount}
|
||||
onChange={event => onAmountChange(event.target.value)}
|
||||
bottomText={errors.amount || warnings.amount || infos.amount}
|
||||
sideAddons={[
|
||||
(
|
||||
<SetMaxAmountButton
|
||||
key="icon"
|
||||
onClick={() => onSetMax()}
|
||||
isActive={setMax}
|
||||
>
|
||||
{!setMax && (
|
||||
<Icon
|
||||
icon={ICONS.TOP}
|
||||
size={25}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
/>
|
||||
)}
|
||||
{setMax && (
|
||||
<Icon
|
||||
icon={ICONS.CHECKED}
|
||||
size={25}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
)}
|
||||
Set max
|
||||
</SetMaxAmountButton>
|
||||
),
|
||||
(
|
||||
<CurrencySelect
|
||||
key="currency"
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
defaultValue={tokensSelectData[0]}
|
||||
isDisabled={tokensSelectData.length < 2}
|
||||
onChange={onCurrencyChange}
|
||||
options={tokensSelectData}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow>
|
||||
<FeeLabelWrapper>
|
||||
<FeeLabel>Fee</FeeLabel>
|
||||
{gasPriceNeedsUpdate && (
|
||||
<UpdateFeeWrapper>
|
||||
<Icon
|
||||
icon={ICONS.WARNING}
|
||||
color={colors.WARNING_PRIMARY}
|
||||
size={20}
|
||||
/>
|
||||
Recommended fees updated. <StyledLink onClick={updateFeeLevels} isGreen>Click here to use them</StyledLink>
|
||||
</UpdateFeeWrapper>
|
||||
)}
|
||||
</FeeLabelWrapper>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
value={selectedFeeLevel}
|
||||
onChange={(option) => {
|
||||
if (option.value === 'Custom') {
|
||||
this.toggleAdvancedSettings();
|
||||
}
|
||||
onFeeLevelChange(option);
|
||||
}}
|
||||
options={feeLevels}
|
||||
formatOptionLabel={option => (
|
||||
<FeeOptionWrapper>
|
||||
<P>{option.value}</P>
|
||||
<P>{option.label}</P>
|
||||
</FeeOptionWrapper>
|
||||
)}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<ToggleAdvancedSettingsWrapper
|
||||
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
|
||||
>
|
||||
<ToggleAdvancedSettingsButton
|
||||
isTransparent
|
||||
onClick={() => this.handleToggleAdvancedSettingsButton()}
|
||||
>
|
||||
Advanced settings
|
||||
<AdvancedSettingsIcon
|
||||
icon={ICONS.ARROW_DOWN}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
isActive={this.state.isAdvancedSettingsHidden}
|
||||
canAnimate={this.state.shouldAnimateAdvancedSettingsToggle}
|
||||
/>
|
||||
</ToggleAdvancedSettingsButton>
|
||||
|
||||
{this.state.isAdvancedSettingsHidden && (
|
||||
<SendButton
|
||||
isDisabled={isSendButtonDisabled}
|
||||
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
|
||||
onClick={() => onSend()}
|
||||
>
|
||||
{sendButtonText}
|
||||
</SendButton>
|
||||
)}
|
||||
</ToggleAdvancedSettingsWrapper>
|
||||
|
||||
{!this.state.isAdvancedSettingsHidden && (
|
||||
<AdvancedSettingsWrapper>
|
||||
<GasInputRow>
|
||||
<GasInput
|
||||
state={this.getGasLimitInputState(errors.gasLimit, warnings.gasLimit)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Gas limit
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Gas limit is the amount of gas to send with your transaction.<br />
|
||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||
Increasing this number will not get your TX mined faster.<br />
|
||||
Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
)}
|
||||
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit}
|
||||
value={gasLimit}
|
||||
isDisabled={networkSymbol === currency && data.length > 0}
|
||||
onChange={event => onGasLimitChange(event.target.value)}
|
||||
/>
|
||||
|
||||
<GasInput
|
||||
state={this.getGasPriceInputState(errors.gasPrice, warnings.gasPrice)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Gas price
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Gas Price is the amount you pay per unit of gas.<br />
|
||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||
Higher the gas price = faster transaction, but more expensive. Recommended is <GreenSpan>{recommendedGasPrice} GWEI.</GreenSpan><br />
|
||||
<Link href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener" isGreen>Read more</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
)}
|
||||
bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice}
|
||||
value={gasPrice}
|
||||
onChange={event => onGasPriceChange(event.target.value)}
|
||||
/>
|
||||
</GasInputRow>
|
||||
|
||||
<StyledTextarea
|
||||
topLabel={(
|
||||
<InputLabelWrapper>
|
||||
Data
|
||||
<Tooltip
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Data is usually used when you send transactions to contracts.
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputLabelWrapper>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAmountInputState(errors.amount, warnings.amount)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<AmountInputLabelWrapper>
|
||||
<AmountInputLabel>Amount</AmountInputLabel>
|
||||
{(isCurrentCurrencyToken && selectedToken) && (
|
||||
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
|
||||
)}
|
||||
disabled={networkSymbol !== currency}
|
||||
value={networkSymbol !== currency ? '' : data}
|
||||
onChange={event => onDataChange(event.target.value)}
|
||||
/>
|
||||
|
||||
<AdvancedSettingsSendButtonWrapper>
|
||||
<SendButton
|
||||
isDisabled={isSendButtonDisabled}
|
||||
onClick={() => onSend()}
|
||||
</AmountInputLabelWrapper>
|
||||
)}
|
||||
value={amount}
|
||||
onChange={event => onAmountChange(event.target.value)}
|
||||
bottomText={errors.amount || warnings.amount || infos.amount}
|
||||
sideAddons={[
|
||||
(
|
||||
<SetMaxAmountButton
|
||||
key="icon"
|
||||
onClick={() => onSetMax()}
|
||||
isActive={setMax}
|
||||
>
|
||||
{sendButtonText}
|
||||
</SendButton>
|
||||
</AdvancedSettingsSendButtonWrapper>
|
||||
</AdvancedSettingsWrapper>
|
||||
)}
|
||||
{!setMax && (
|
||||
<Icon
|
||||
icon={ICONS.TOP}
|
||||
size={25}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
/>
|
||||
)}
|
||||
{setMax && (
|
||||
<Icon
|
||||
icon={ICONS.CHECKED}
|
||||
size={25}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
)}
|
||||
Set max
|
||||
</SetMaxAmountButton>
|
||||
),
|
||||
(
|
||||
<CurrencySelect
|
||||
key="currency"
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
defaultValue={tokensSelectData[0]}
|
||||
isDisabled={tokensSelectData.length < 2}
|
||||
onChange={onCurrencyChange}
|
||||
options={tokensSelectData}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
{this.props.selectedAccount.pending.length > 0 && (
|
||||
<PendingTransactions
|
||||
pending={this.props.selectedAccount.pending}
|
||||
tokens={this.props.selectedAccount.tokens}
|
||||
network={network}
|
||||
<InputRow>
|
||||
<FeeLabelWrapper>
|
||||
<FeeLabel>Fee</FeeLabel>
|
||||
{gasPriceNeedsUpdate && (
|
||||
<UpdateFeeWrapper>
|
||||
<Icon
|
||||
icon={ICONS.WARNING}
|
||||
color={colors.WARNING_PRIMARY}
|
||||
size={20}
|
||||
/>
|
||||
Recommended fees updated. <StyledLink onClick={updateFeeLevels} isGreen>Click here to use them</StyledLink>
|
||||
</UpdateFeeWrapper>
|
||||
)}
|
||||
</FeeLabelWrapper>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
value={selectedFeeLevel}
|
||||
onChange={onFeeLevelChange}
|
||||
options={feeLevels}
|
||||
formatOptionLabel={option => (
|
||||
<FeeOptionWrapper>
|
||||
<P>{option.value}</P>
|
||||
<P>{option.label}</P>
|
||||
</FeeOptionWrapper>
|
||||
)}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<ToggleAdvancedSettingsWrapper
|
||||
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||
>
|
||||
<ToggleAdvancedSettingsButton
|
||||
isTransparent
|
||||
onClick={toggleAdvanced}
|
||||
>
|
||||
Advanced settings
|
||||
<AdvancedSettingsIcon
|
||||
icon={ICONS.ARROW_DOWN}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
isActive={advanced}
|
||||
canAnimate
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</SelectedAccount>
|
||||
);
|
||||
}
|
||||
}
|
||||
</ToggleAdvancedSettingsButton>
|
||||
|
||||
{isAdvancedSettingsHidden && (
|
||||
<SendButton
|
||||
isDisabled={isSendButtonDisabled}
|
||||
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||
onClick={() => onSend()}
|
||||
>
|
||||
{sendButtonText}
|
||||
</SendButton>
|
||||
)}
|
||||
</ToggleAdvancedSettingsWrapper>
|
||||
|
||||
{advanced && (
|
||||
<AdvancedForm {...props}>
|
||||
<SendButton
|
||||
isDisabled={isSendButtonDisabled}
|
||||
onClick={() => onSend()}
|
||||
>
|
||||
{sendButtonText}
|
||||
</SendButton>
|
||||
</AdvancedForm>
|
||||
)}
|
||||
|
||||
{props.selectedAccount.pending.length > 0 && (
|
||||
<PendingTransactions
|
||||
pending={props.selectedAccount.pending}
|
||||
tokens={props.selectedAccount.tokens}
|
||||
network={network}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</SelectedAccount>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSend;
|
||||
|
@ -2,8 +2,10 @@ import webpack from 'webpack';
|
||||
import GitRevisionPlugin from 'git-revision-webpack-plugin';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import FlowWebpackPlugin from 'flow-webpack-plugin';
|
||||
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
|
||||
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
// turn on for bundle analyzing
|
||||
// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
|
||||
import {
|
||||
SRC, BUILD, PORT, PUBLIC,
|
||||
@ -94,6 +96,10 @@ module.exports = {
|
||||
hints: false,
|
||||
},
|
||||
plugins: [
|
||||
new WebpackBuildNotifierPlugin({
|
||||
title: 'Trezor Wallet',
|
||||
suppressSuccess: true,
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
|
||||
}),
|
||||
@ -107,11 +113,11 @@ module.exports = {
|
||||
inject: true,
|
||||
favicon: `${SRC}images/favicon.ico`,
|
||||
}),
|
||||
new BundleAnalyzerPlugin({
|
||||
openAnalyzer: false,
|
||||
analyzerMode: false, // turn on to generate bundle pass 'static'
|
||||
reportFilename: 'bundle-report.html',
|
||||
}),
|
||||
// new BundleAnalyzerPlugin({
|
||||
// openAnalyzer: false,
|
||||
// analyzerMode: false, // turn on to generate bundle pass 'static'
|
||||
// reportFilename: 'bundle-report.html',
|
||||
// }),
|
||||
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
@ -3,7 +3,6 @@ import webpack from 'webpack';
|
||||
import GitRevisionPlugin from 'git-revision-webpack-plugin';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import FlowWebpackPlugin from 'flow-webpack-plugin';
|
||||
import { SRC, BUILD, PUBLIC } from './constants';
|
||||
|
||||
const gitRevisionPlugin = new GitRevisionPlugin();
|
||||
@ -65,9 +64,9 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
|
||||
'process.env.BUILD': JSON.stringify(process.env.BUILD),
|
||||
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()
|
||||
}),
|
||||
new FlowWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
chunks: ['index'],
|
||||
template: `${SRC}index.html`,
|
||||
|
83
yarn.lock
83
yarn.lock
@ -2736,6 +2736,10 @@ cors@^2.8.1:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
corser@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
||||
|
||||
cosmiconfig@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397"
|
||||
@ -2803,6 +2807,13 @@ create-hmac@^1.1.4:
|
||||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
cross-env@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
|
||||
dependencies:
|
||||
cross-spawn "^6.0.5"
|
||||
is-windows "^1.0.0"
|
||||
|
||||
cross-spawn@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
|
||||
@ -3399,6 +3410,15 @@ ecc-jsbn@~0.1.1:
|
||||
dependencies:
|
||||
jsbn "~0.1.0"
|
||||
|
||||
ecstatic@^3.0.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.0.tgz#91cd417d152abf85b37b1ab3ebf3bd25cdc64e80"
|
||||
dependencies:
|
||||
he "^1.1.1"
|
||||
mime "^1.6.0"
|
||||
minimist "^1.1.0"
|
||||
url-join "^2.0.5"
|
||||
|
||||
editions@^1.3.3:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
|
||||
@ -4895,7 +4915,7 @@ hdkey@^0.8.0:
|
||||
safe-buffer "^5.1.1"
|
||||
secp256k1 "^3.0.1"
|
||||
|
||||
he@1.1.x:
|
||||
he@1.1.x, he@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||
|
||||
@ -5060,7 +5080,7 @@ http-proxy-middleware@~0.18.0:
|
||||
lodash "^4.17.5"
|
||||
micromatch "^3.1.9"
|
||||
|
||||
http-proxy@^1.16.2:
|
||||
http-proxy@^1.16.2, http-proxy@^1.8.1:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
|
||||
dependencies:
|
||||
@ -5068,6 +5088,19 @@ http-proxy@^1.16.2:
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-server@^0.11.1:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b"
|
||||
dependencies:
|
||||
colors "1.0.3"
|
||||
corser "~2.0.0"
|
||||
ecstatic "^3.0.0"
|
||||
http-proxy "^1.8.1"
|
||||
opener "~1.4.0"
|
||||
optimist "0.6.x"
|
||||
portfinder "^1.0.13"
|
||||
union "~0.4.3"
|
||||
|
||||
http-signature@~1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
|
||||
@ -5567,7 +5600,7 @@ is-whitespace-character@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed"
|
||||
|
||||
is-windows@^1.0.1, is-windows@^1.0.2:
|
||||
is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
||||
@ -6746,6 +6779,10 @@ mime@1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
||||
|
||||
mime@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
||||
mime@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
|
||||
@ -6797,7 +6834,7 @@ minimist@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
|
||||
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
@ -7021,7 +7058,7 @@ node-libs-browser@^2.0.0:
|
||||
util "^0.10.3"
|
||||
vm-browserify "0.0.4"
|
||||
|
||||
node-notifier@^5.2.1:
|
||||
node-notifier@5.2.1, node-notifier@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
|
||||
dependencies:
|
||||
@ -7298,13 +7335,17 @@ opener@^1.4.3:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
|
||||
|
||||
opener@~1.4.0:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
|
||||
|
||||
opn@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
|
||||
dependencies:
|
||||
is-wsl "^1.1.0"
|
||||
|
||||
optimist@^0.6.1:
|
||||
optimist@0.6.x, optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
dependencies:
|
||||
@ -7664,6 +7705,14 @@ pn@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
|
||||
|
||||
portfinder@^1.0.13:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a"
|
||||
dependencies:
|
||||
async "^1.5.2"
|
||||
debug "^2.2.0"
|
||||
mkdirp "0.5.x"
|
||||
|
||||
portfinder@^1.0.9:
|
||||
version "1.0.13"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
|
||||
@ -8172,6 +8221,10 @@ qs@6.5.2, qs@~6.5.1, qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
||||
qs@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404"
|
||||
|
||||
qs@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
@ -10370,6 +10423,12 @@ union-value@^1.0.0:
|
||||
is-extendable "^0.1.1"
|
||||
set-value "^0.4.3"
|
||||
|
||||
union@~0.4.3:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/union/-/union-0.4.6.tgz#198fbdaeba254e788b0efcb630bc11f24a2959e0"
|
||||
dependencies:
|
||||
qs "~2.3.3"
|
||||
|
||||
uniq@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
|
||||
@ -10467,6 +10526,10 @@ urix@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
|
||||
url-join@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
|
||||
|
||||
url-join@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
|
||||
@ -10906,6 +10969,14 @@ webpack-addons@^1.1.5:
|
||||
dependencies:
|
||||
jscodeshift "^0.4.0"
|
||||
|
||||
webpack-build-notifier@^0.1.29:
|
||||
version "0.1.29"
|
||||
resolved "https://registry.yarnpkg.com/webpack-build-notifier/-/webpack-build-notifier-0.1.29.tgz#d71f89bb94346c6b748e07aa3d117d2beb0a151f"
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
node-notifier "5.2.1"
|
||||
strip-ansi "^3.0.1"
|
||||
|
||||
webpack-bundle-analyzer@^2.13.1:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.1.tgz#07d2176c6e86c3cdce4c23e56fae2a7b6b4ad526"
|
||||
|
Loading…
Reference in New Issue
Block a user