Merge pull request #508 from MyEtherWallet/develop

Release 0.0.5
This commit is contained in:
Daniel Ternyak 2017-12-01 10:41:24 -06:00 committed by GitHub
commit 38f09e231c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 4714 additions and 1402 deletions

View File

@ -7,6 +7,8 @@ before_install:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
install:
- npm install --silent
notifications:
email:
@ -16,4 +18,6 @@ notifications:
script:
- npm run test
- npm run tslint
- tsc --noEmit
- npm run tscheck
- npm run freezer
- npm run freezer:validate

View File

@ -334,7 +334,36 @@ When working on a module that has styling in Less, try to do the following:
* Convert as many `<br/>` tags or `&nbsp;`s to margins
* Ensure that there has been little to no deviation from screenshot
#### Adding Icon-fonts
1. Download chosen icon-font
1. Declare css font-family:
```
@font-face {
font-family: 'social-media';
src: url('../assets/fonts/social-media.eot');
src: url('../assets/fonts/social-media.eot') format('embedded-opentype'),
url('../assets/fonts/social-media.woff2') format('woff2'),
url('../assets/fonts/social-media.woff') format('woff'),
url('../assets/fonts/social-media.ttf') format('truetype'),
url('../assets/fonts/social-media.svg') format('svg');
font-weight: normal;
font-style: normal;
}
```
1. Create classes for each icon using their unicode character
```
.sm-logo-facebook:before {
content: '\ea02';
}
```
* [How to get unicode icon values?](https://stackoverflow.com/questions/27247145/get-the-unicode-icon-value-from-a-custom-font)
1. Write some markup:
```
<a href="/">
<i className={`sm-icon sm-logo-${text} sm-24px`} />
Hello World
</a>
```
## Thanks & Support

View File

@ -11,6 +11,7 @@ import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
import RestoreKeystore from 'containers/Tabs/RestoreKeystore';
// TODO: fix this
interface Props {
@ -33,12 +34,12 @@ export default class Root extends Component<Props, {}> {
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route path="/utilities" component={RestoreKeystore} />
<Route
path="/sign-and-verify-message"
component={SignAndVerifyMessage}
/>
<Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes />
</div>
</Router>

View File

@ -1,6 +1,6 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig } from 'config/data';
import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
export type TForceOfflineConfig = typeof forceOfflineConfig;
export function forceOfflineConfig(): interfaces.ForceOfflineAction {
@ -80,6 +80,26 @@ export function removeCustomNode(
};
}
export type TAddCustomNetwork = typeof addCustomNetwork;
export function addCustomNetwork(
payload: CustomNetworkConfig
): interfaces.AddCustomNetworkAction {
return {
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK,
payload
};
}
export type TRemoveCustomNetwork = typeof removeCustomNetwork;
export function removeCustomNetwork(
payload: CustomNetworkConfig
): interfaces.RemoveCustomNetworkAction {
return {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK,
payload
};
}
export type TSetLatestBlock = typeof setLatestBlock;
export function setLatestBlock(
payload: string

View File

@ -1,5 +1,5 @@
import { TypeKeys } from './constants';
import { CustomNodeConfig, NodeConfig } from 'config/data';
import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
/*** Toggle Offline ***/
export interface ToggleOfflineAction {
@ -56,6 +56,18 @@ export interface RemoveCustomNodeAction {
payload: CustomNodeConfig;
}
/*** Add Custom Network ***/
export interface AddCustomNetworkAction {
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK;
payload: CustomNetworkConfig;
}
/*** Remove Custom Network ***/
export interface RemoveCustomNetworkAction {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK;
payload: CustomNetworkConfig;
}
/*** Set Latest Block ***/
export interface SetLatestBlockAction {
type: TypeKeys.CONFIG_SET_LATEST_BLOCK;
@ -78,5 +90,7 @@ export type ConfigAction =
| ChangeNodeIntentAction
| AddCustomNodeAction
| RemoveCustomNodeAction
| AddCustomNetworkAction
| RemoveCustomNetworkAction
| SetLatestBlockAction
| Web3UnsetNodeAction;

View File

@ -8,6 +8,8 @@ export enum TypeKeys {
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
CONFIG_ADD_CUSTOM_NETWORK = 'CONFIG_ADD_CUSTOM_NETWORK',
CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK',
CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK',
CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET'
}

View File

@ -6,7 +6,7 @@ export type TShowNotification = typeof showNotification;
export function showNotification(
level: types.NOTIFICATION_LEVEL = 'info',
msg: ReactElement<any> | string,
duration?: number | types.INFINITY
duration?: number
): types.ShowNotificationAction {
return {
type: TypeKeys.SHOW_NOTIFICATION,

View File

@ -2,13 +2,12 @@ import { ReactElement } from 'react';
import { TypeKeys } from './constants';
/*** Shared types ***/
export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info';
export type INFINITY = 'infinity';
export interface Notification {
level: NOTIFICATION_LEVEL;
msg: ReactElement<any> | string;
id: number;
duration?: number | INFINITY;
duration?: number;
}
/*** Close notification ***/

View File

@ -26,6 +26,11 @@ export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
fetch(CCRates(symbols))
.then(response => handleJSONResponse(response, ERROR_MESSAGE))
.then(rates => {
// API errors come as 200s, so check the json for error
if (rates.Response && rates.Response === 'Error') {
throw new Error('Failed to fetch rates');
}
// All currencies are in ETH right now. We'll do token -> eth -> value to
// do it all in one request
// to their respective rates via ETH.

View File

@ -69,14 +69,14 @@ export interface BityOrderCreateRequestedSwapAction {
};
}
interface BityOrderInput {
export interface BityOrderInput {
amount: string;
currency: string;
reference: string;
status: string;
}
interface BityOrderOutput {
export interface BityOrderOutput {
amount: string;
currency: string;
reference: string;

View File

@ -71,7 +71,10 @@ export function setBalanceRejected(): types.SetBalanceRejectedAction {
export type TSetTokenBalances = typeof setTokenBalances;
export function setTokenBalances(payload: {
[key: string]: TokenValue;
[key: string]: {
balance: TokenValue;
error: string | null;
};
}): types.SetTokenBalancesAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES,

View File

@ -48,7 +48,10 @@ export interface SetBalanceRejectedAction {
export interface SetTokenBalancesAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
payload: {
[key: string]: TokenValue;
[key: string]: {
balance: TokenValue;
error: string | null;
};
};
}

View File

@ -3,8 +3,8 @@ import { checkHttpStatus, parseJSON } from './utils';
export function getAllRates() {
const mappedRates = {};
return _getAllRates().then((bityRates) => {
bityRates.objects.forEach((each) => {
return _getAllRates().then(bityRates => {
bityRates.objects.forEach(each => {
const pairName = each.pair;
mappedRates[pairName] = parseFloat(each.rate_we_sell);
});
@ -26,7 +26,7 @@ export function postOrder(
mode,
pair
}),
headers: bityConfig.postConfig.headers
headers: new Headers(bityConfig.postConfig.headers)
})
.then(checkHttpStatus)
.then(parseJSON);
@ -38,7 +38,7 @@ export function getOrderStatus(orderId: string) {
body: JSON.stringify({
orderid: orderId
}),
headers: bityConfig.postConfig.headers
headers: new Headers(bityConfig.postConfig.headers)
})
.then(checkHttpStatus)
.then(parseJSON);
@ -48,4 +48,4 @@ function _getAllRates() {
return fetch(`${bityConfig.bityURL}/v1/rate2/`)
.then(checkHttpStatus)
.then(parseJSON);
}
}

Binary file not shown.

View File

@ -0,0 +1,36 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<font id="social-media" horiz-adv-x="256">
<font-face font-family="social-media"
units-per-em="256" ascent="256"
descent="0" />
<missing-glyph horiz-adv-x="0" />
<glyph glyph-name="logo-facebook"
unicode="&#xEA02;"
horiz-adv-x="256" d="M234.6666666666667 256H21.3333333333333C9.5466666666667 256 0 246.4533333333333 0 234.6666666666667V21.3333333333333C0 9.5466666666667 9.5466666666667 0 21.3333333333333 0H138.6666666666667V96H106.6666666666667V138.6666666666667H138.6666666666667V166.2613333333334C138.6666666666667 199.328 158.8586666666667 217.3333333333334 188.3626666666667 217.3333333333334C202.496 217.3333333333334 214.6346666666667 216.2773333333333 218.176 215.808V181.248L197.7173333333333 181.2373333333334C181.6746666666667 181.2373333333334 178.5706666666667 173.6106666666667 178.5706666666667 162.432V138.6666666666667H225.9306666666667L215.264 96H178.5706666666667V0H234.6666666666667C246.4533333333333 0 256 9.5466666666667 256 21.3333333333333V234.6666666666667C256 246.4533333333333 246.4533333333333 256 234.6666666666667 256z" />
<glyph glyph-name="logo-reddit"
unicode="&#xEA03;"
horiz-adv-x="256" d="M256 130.1333333333333C256 147.2 242.1333333333334 161.0666666666667 225.0666666666667 161.0666666666667C217.6 161.0666666666667 211.2 158.9333333333333 205.8666666666667 154.6666666666667C186.6666666666667 166.4 163.2 173.8666666666667 137.6 174.9333333333333L150.4 216.5333333333333L186.6666666666667 208C187.7333333333334 195.2 198.4 184.5333333333333 212.2666666666667 184.5333333333333C226.1333333333334 184.5333333333333 237.8666666666667 196.2666666666667 237.8666666666667 210.1333333333333C237.8666666666667 224 226.1333333333334 235.7333333333333 212.2666666666667 235.7333333333333C202.6666666666667 235.7333333333333 194.1333333333334 230.4 189.8666666666667 221.8666666666667L147.2 231.4666666666667C144 232.5333333333333 139.7333333333334 230.4 138.6666666666667 227.2L122.6666666666667 174.9333333333333C96 173.8666666666667 69.3333333333333 167.4666666666667 50.1333333333333 154.6666666666667C44.8 158.9333333333333 38.4 161.0666666666667 30.9333333333333 161.0666666666667C13.8666666666667 161.0666666666667 0 147.2 0 130.1333333333333C0 119.4666666666667 5.3333333333333 109.8666666666667 12.8 104.5333333333333C12.8 102.4 12.8 99.2 12.8 97.0666666666667C12.8 75.7333333333333 25.6 56.5333333333333 48 41.6C69.3333333333333 27.7333333333333 98.1333333333333 20.2666666666667 128 20.2666666666667C157.8666666666667 20.2666666666667 186.6666666666667 27.7333333333333 208 41.6C230.4 56.5333333333333 243.2 75.7333333333333 243.2 97.0666666666667C243.2 99.2 243.2 101.3333333333333 243.2 103.4666666666667C250.6666666666667 109.8666666666667 256 119.4666666666667 256 130.1333333333333zM213.3333333333333 221.8666666666667C219.7333333333333 221.8666666666667 225.0666666666667 216.5333333333333 225.0666666666667 210.1333333333333C225.0666666666667 203.7333333333334 219.7333333333333 198.4 213.3333333333333 198.4S201.6 203.7333333333334 201.6 210.1333333333333C201.6 216.5333333333333 206.9333333333333 221.8666666666667 213.3333333333333 221.8666666666667zM72.5333333333333 110.9333333333333C72.5333333333333 120.5333333333333 81.0666666666667 128 89.6 128C99.2 128 106.6666666666667 119.4666666666667 106.6666666666667 110.9333333333333S99.2 93.8666666666667 89.6 93.8666666666667C81.0666666666667 93.8666666666667 72.5333333333333 101.3333333333333 72.5333333333333 110.9333333333333zM170.6666666666667 60.8C162.1333333333333 52.2666666666667 148.2666666666667 48 129.0666666666667 48C129.0666666666667 48 129.0666666666667 48 129.0666666666667 48C129.0666666666667 48 129.0666666666667 48 129.0666666666667 48C109.8666666666667 48 96 52.2666666666667 87.4666666666667 60.8C84.2666666666667 64 84.2666666666667 68.2666666666667 87.4666666666667 70.4C90.6666666666667 73.6 94.9333333333333 73.6 97.0666666666667 70.4C103.4666666666667 64 114.1333333333333 60.8 129.0666666666667 60.8C129.0666666666667 60.8 129.0666666666667 60.8 129.0666666666667 60.8C129.0666666666667 60.8 129.0666666666667 60.8 129.0666666666667 60.8C144 60.8 154.6666666666667 64 161.0666666666667 70.4C164.2666666666667 73.6 168.5333333333333 73.6 170.6666666666667 70.4C172.8 67.2 172.8 62.9333333333333 170.6666666666667 60.8zM166.4 93.8666666666667C156.8 93.8666666666667 148.2666666666667 101.3333333333333 148.2666666666667 110.9333333333333S156.8 128 166.4 128C176 128 183.4666666666667 119.4666666666667 183.4666666666667 110.9333333333333S176 93.8666666666667 166.4 93.8666666666667z" />
<glyph glyph-name="logo-github"
unicode="&#xEA04;"
horiz-adv-x="256" d="M128 252.8C57.6 252.8 0 195.2 0 124.8C0 68.2666666666667 36.2666666666667 20.2666666666667 87.4666666666667 3.2C93.8666666666667 2.1333333333333 96 6.4 96 9.6C96 12.8 96 20.2666666666667 96 30.9333333333333C60.8 23.4666666666667 53.3333333333333 48 53.3333333333333 48C48 62.9333333333333 39.4666666666667 67.2 39.4666666666667 67.2C26.6666666666667 74.6666666666667 39.4666666666667 74.6666666666667 39.4666666666667 74.6666666666667C52.2666666666667 73.6 58.6666666666667 61.8666666666667 58.6666666666667 61.8666666666667C70.4 42.6666666666667 88.5333333333333 48 96 51.2C97.0666666666667 59.7333333333334 100.2666666666667 65.0666666666667 104.5333333333333 68.2666666666667C75.7333333333333 71.4666666666667 45.8666666666667 82.1333333333334 45.8666666666667 131.2C45.8666666666667 145.0666666666667 51.2 156.8 58.6666666666667 165.3333333333334C58.6666666666667 169.6 53.3333333333333 182.4 60.8 199.4666666666667C60.8 199.4666666666667 71.4666666666667 202.6666666666667 96 186.6666666666667C106.6666666666667 189.8666666666667 117.3333333333333 190.9333333333333 128 190.9333333333333C138.6666666666667 190.9333333333333 149.3333333333334 189.8666666666667 160 186.6666666666667C184.5333333333333 203.7333333333334 195.2 199.4666666666667 195.2 199.4666666666667C202.6666666666667 181.3333333333334 197.3333333333333 168.5333333333333 196.2666666666667 165.3333333333334C204.8 156.8 209.0666666666667 145.0666666666667 209.0666666666667 131.2C209.0666666666667 82.1333333333334 179.2 71.4666666666667 150.4 68.2666666666667C154.6666666666667 64 158.9333333333333 56.5333333333334 158.9333333333333 44.8C158.9333333333333 27.7333333333334 158.9333333333333 13.8666666666667 158.9333333333333 9.6C158.9333333333333 6.4 161.0666666666667 2.1333333333334 167.4666666666667 3.2C218.6666666666667 20.2666666666667 254.9333333333334 68.2666666666667 254.9333333333334 124.8C256 195.2 198.4 252.8 128 252.8z" />
<glyph glyph-name="logo-twitter"
unicode="&#xEA05;"
horiz-adv-x="256" d="M256 206.9333333333333C246.4 202.6666666666667 236.8 199.4666666666667 226.1333333333334 198.4C236.8 204.8 245.3333333333333 215.4666666666667 249.6 227.2C238.9333333333334 220.8 228.2666666666667 216.5333333333333 216.5333333333333 214.4C206.9333333333333 225.0666666666667 193.0666666666667 231.4666666666667 178.1333333333333 231.4666666666667C149.3333333333333 231.4666666666667 125.8666666666667 208 125.8666666666667 179.2C125.8666666666667 174.9333333333333 125.8666666666667 170.6666666666667 126.9333333333333 167.4666666666667C82.1333333333333 169.6 43.7333333333333 190.9333333333333 18.1333333333333 222.9333333333333C12.8 214.4 10.6666666666667 205.8666666666667 10.6666666666667 196.2666666666667C10.6666666666667 178.1333333333333 20.2666666666667 162.1333333333333 34.1333333333333 152.5333333333334C25.6 152.5333333333334 17.0666666666667 154.6666666666667 10.6666666666667 158.9333333333333C10.6666666666667 158.9333333333333 10.6666666666667 158.9333333333333 10.6666666666667 157.8666666666667C10.6666666666667 132.2666666666667 28.8 110.9333333333333 52.2666666666667 106.6666666666667C48 105.6 43.7333333333333 104.5333333333334 38.4 104.5333333333334C35.2 104.5333333333334 32 104.5333333333334 28.8 105.6C35.2 84.2666666666667 54.4 69.3333333333334 77.8666666666667 69.3333333333334C59.7333333333333 55.4666666666667 37.3333333333333 46.9333333333334 12.8 46.9333333333334C8.5333333333333 46.9333333333334 4.2666666666667 46.9333333333334 0 48C23.4666666666667 33.0666666666667 51.2 24.5333333333334 80 24.5333333333334C177.0666666666667 24.5333333333334 229.3333333333333 104.5333333333334 229.3333333333333 173.8666666666667C229.3333333333333 176 229.3333333333333 178.1333333333333 229.3333333333333 180.2666666666667C240 187.7333333333334 248.5333333333334 197.3333333333334 256 206.9333333333333z" />
<glyph glyph-name="logo-linkedin"
unicode="&#xEA06;"
horiz-adv-x="256" d="M245.3333333333333 256H10.6666666666667C4.2666666666667 256 0 251.7333333333333 0 245.3333333333334V10.6666666666667C0 4.2666666666667 4.2666666666667 0 10.6666666666667 0H245.3333333333333C251.7333333333333 0 256 4.2666666666667 256 10.6666666666667V245.3333333333334C256 251.7333333333333 251.7333333333334 256 245.3333333333333 256zM75.7333333333333 37.3333333333333H38.4V160H76.8V37.3333333333333zM56.5333333333333 177.0666666666667C44.8 177.0666666666667 34.1333333333333 186.6666666666667 34.1333333333333 199.4666666666667C34.1333333333333 211.2 43.7333333333333 221.8666666666667 56.5333333333333 221.8666666666667C68.2666666666667 221.8666666666667 78.9333333333333 212.2666666666667 78.9333333333333 199.4666666666667C78.9333333333333 186.6666666666667 69.3333333333333 177.0666666666667 56.5333333333333 177.0666666666667zM218.6666666666667 37.3333333333333H180.2666666666667V97.0666666666667C180.2666666666667 110.9333333333333 180.2666666666667 129.0666666666667 161.0666666666667 129.0666666666667C140.8 129.0666666666667 138.6666666666667 114.1333333333333 138.6666666666667 98.1333333333333V37.3333333333333H100.2666666666667V160H136.5333333333333V142.9333333333333H136.5333333333333C141.8666666666667 152.5333333333333 153.6 162.1333333333333 172.8 162.1333333333333C211.2 162.1333333333333 218.6666666666667 136.5333333333334 218.6666666666667 103.4666666666667V37.3333333333333z" />
<glyph glyph-name="logo-slack"
unicode="&#xEA07;"
horiz-adv-x="256" d="M116.2231466666667 106.05504L105.0436266666667 139.43168L139.43168 150.9499733333333L150.6112 117.5733333333334L116.2231466666667 106.05504zM116.2231466666667 106.05504L105.0436266666667 139.43168L139.43168 150.9499733333333L150.6112 117.5733333333334L116.2231466666667 106.05504zM245.3333333333333 163.2C218.6666666666667 250.6666666666667 180.2666666666667 272 92.8 245.3333333333334C5.3333333333333 218.6666666666667 -16 180.2666666666667 10.6666666666667 92.8C37.3333333333333 5.3333333333334 74.6666666666667 -16 163.2 10.6666666666667C250.6666666666667 37.3333333333333 272 75.7333333333334 245.3333333333333 163.2zM200.5333333333333 105.6L183.4666666666667 100.2666666666667L188.8 83.2C190.9333333333333 75.7333333333334 187.7333333333334 68.2666666666667 180.2666666666667 66.1333333333334C179.2 66.1333333333334 177.0666666666667 65.0666666666667 176 65.0666666666667C170.6666666666667 65.0666666666667 165.3333333333334 68.2666666666667 164.2666666666667 73.6L158.9333333333333 90.6666666666667L124.8 78.9333333333333L130.1333333333333 61.8666666666667C132.2666666666667 54.4 129.0666666666667 46.9333333333333 121.6 44.8C120.5333333333333 44.8 118.4 43.7333333333334 117.3333333333333 43.7333333333334C112 43.7333333333334 106.6666666666667 46.9333333333333 105.6 52.2666666666667L100.2666666666667 69.3333333333333L83.2 64C82.1333333333333 64 80 62.9333333333333 78.9333333333333 62.9333333333333C73.6 62.9333333333333 68.2666666666667 66.1333333333333 67.2 71.4666666666667C64 82.1333333333333 67.2 89.6 74.6666666666667 91.7333333333334L91.7333333333333 97.0666666666667L81.0666666666667 130.1333333333334L64 124.8C62.9333333333333 124.8 60.8 123.7333333333334 59.7333333333333 123.7333333333334C54.4 123.7333333333334 49.0666666666667 126.9333333333333 48 132.2666666666667C45.8666666666667 139.7333333333334 49.0666666666667 147.2 56.5333333333333 149.3333333333334L73.6 154.6666666666667L66.1333333333333 172.8C64 179.2 67.2 186.6666666666667 74.6666666666667 189.8666666666667C81.0666666666667 192 88.5333333333333 187.7333333333334 91.7333333333333 181.3333333333334L97.0666666666667 164.2666666666667L131.2 176L125.8666666666667 192C123.7333333333333 199.4666666666667 126.9333333333333 206.9333333333333 134.4 209.0666666666667C141.8666666666667 211.2 149.3333333333334 208 151.4666666666667 200.5333333333333L156.8 183.4666666666667L173.8666666666667 188.8C180.2666666666667 192 187.7333333333334 187.7333333333334 189.8666666666667 181.3333333333334C192 173.8666666666667 188.8 166.4 181.3333333333333 164.2666666666667L164.2666666666667 158.9333333333333L174.9333333333333 125.8666666666667L192 131.2C199.4666666666667 133.3333333333334 206.9333333333333 130.1333333333333 209.0666666666666 122.6666666666667C211.2 116.2666666666667 208 108.8 200.5333333333333 105.6z" />
<glyph glyph-name="logo-medium"
unicode="&#xEA08;"
horiz-adv-x="256" d="M245.3333333333333 256H10.6666666666667A10.666666666666666 10.666666666666666 0 0 1 0 245.3333333333334V10.6666666666667A10.666666666666666 10.666666666666666 0 0 1 10.6666666666667 0H245.3333333333333A10.666666666666666 10.666666666666666 0 0 1 256 10.6666666666667V245.3333333333334A10.666666666666666 10.666666666666666 0 0 1 245.3333333333333 256zM212.672 195.3493333333333L198.944 182.1866666666667A4.010666666666666 4.010666666666666 0 0 1 197.4186666666667 178.336V81.6106666666667A4.010666666666666 4.010666666666666 0 0 1 198.944 77.76L212.352 64.5973333333334V61.7066666666667H144.9173333333333V64.5973333333334L158.784 78.08C160.1493333333333 79.4453333333333 160.1493333333333 79.8506666666667 160.1493333333333 81.9306666666667V160.1066666666667L121.5253333333333 61.9733333333334H116.3093333333333L71.3706666666667 160.1066666666667V94.368A9.066666666666666 9.066666666666666 0 0 1 73.856 86.8266666666667L91.9253333333333 64.9173333333333V62.0266666666666H40.7253333333333V64.9173333333333L58.7626666666667 86.8266666666667A8.746666666666666 8.746666666666666 0 0 1 61.088 94.368V170.3786666666667A6.666666666666666 6.666666666666666 0 0 1 58.9546666666667 176L42.8693333333333 195.3493333333333V198.24H92.7253333333333L131.264 113.7173333333333L165.1413333333333 198.24H212.672z" />
<glyph glyph-name="logo-github-2"
unicode="&#xEA09;"
horiz-adv-x="256" d="M128 252.8C57.6 252.8 0 195.2 0 124.8C0 68.2666666666667 36.2666666666667 20.2666666666667 87.4666666666667 3.2C93.8666666666667 2.1333333333333 96 6.4 96 9.6C96 12.8 96 20.2666666666667 96 30.9333333333333C60.8 23.4666666666667 53.3333333333333 48 53.3333333333333 48C48 62.9333333333333 39.4666666666667 67.2 39.4666666666667 67.2C26.6666666666667 74.6666666666667 39.4666666666667 74.6666666666667 39.4666666666667 74.6666666666667C52.2666666666667 73.6 58.6666666666667 61.8666666666667 58.6666666666667 61.8666666666667C70.4 42.6666666666667 88.5333333333333 48 96 51.2C97.0666666666667 59.7333333333334 100.2666666666667 65.0666666666667 104.5333333333333 68.2666666666667C75.7333333333333 71.4666666666667 45.8666666666667 82.1333333333334 45.8666666666667 131.2C45.8666666666667 145.0666666666667 51.2 156.8 58.6666666666667 165.3333333333334C58.6666666666667 169.6 53.3333333333333 182.4 60.8 199.4666666666667C60.8 199.4666666666667 71.4666666666667 202.6666666666667 96 186.6666666666667C106.6666666666667 189.8666666666667 117.3333333333333 190.9333333333333 128 190.9333333333333C138.6666666666667 190.9333333333333 149.3333333333334 189.8666666666667 160 186.6666666666667C184.5333333333333 203.7333333333334 195.2 199.4666666666667 195.2 199.4666666666667C202.6666666666667 181.3333333333334 197.3333333333333 168.5333333333333 196.2666666666667 165.3333333333334C204.8 156.8 209.0666666666667 145.0666666666667 209.0666666666667 131.2C209.0666666666667 82.1333333333334 179.2 71.4666666666667 150.4 68.2666666666667C154.6666666666667 64 158.9333333333333 56.5333333333334 158.9333333333333 44.8C158.9333333333333 27.7333333333334 158.9333333333333 13.8666666666667 158.9333333333333 9.6C158.9333333333333 6.4 161.0666666666667 2.1333333333334 167.4666666666667 3.2C218.6666666666667 20.2666666666667 254.9333333333334 68.2666666666667 254.9333333333334 124.8C256 195.2 198.4 252.8 128 252.8z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92 92"><path fill="#0e97c0" d="M45.386.004C19.983.344-.333 21.215.005 46.619c.34 25.393 21.209 45.715 46.611 45.377 25.398-.342 45.718-21.213 45.38-46.615-.34-25.395-21.21-45.716-46.61-45.377zM45.25 74l-.254-.004c-3.912-.116-6.67-2.998-6.559-6.852.109-3.788 2.934-6.538 6.717-6.538l.227.004c4.021.119 6.748 2.972 6.635 6.937C51.904 71.346 49.123 74 45.25 74zm16.455-32.659c-.92 1.307-2.943 2.93-5.492 4.916l-2.807 1.938c-1.541 1.198-2.471 2.325-2.82 3.434-.275.873-.41 1.104-.434 2.88l-.004.451H39.43l.031-.907c.131-3.728.223-5.921 1.768-7.733 2.424-2.846 7.771-6.289 7.998-6.435.766-.577 1.412-1.234 1.893-1.936 1.125-1.551 1.623-2.772 1.623-3.972a7.74 7.74 0 0 0-1.471-4.576c-.939-1.323-2.723-1.993-5.303-1.993-2.559 0-4.311.812-5.359 2.478-1.078 1.713-1.623 3.512-1.623 5.35v.457H27.936l.02-.477c.285-6.769 2.701-11.643 7.178-14.487C37.947 18.918 41.447 18 45.531 18c5.346 0 9.859 1.299 13.412 3.861 3.6 2.596 5.426 6.484 5.426 11.556 0 2.837-.896 5.502-2.664 7.924z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -135,52 +135,6 @@ textarea {
}
}
.account-help-icon {
h3,
h4,
h5,
h6,
img {
display: inline-block;
}
img:hover + .account-help-text {
display: block;
}
img {
margin-left: -30px;
margin-right: 3px;
}
}
.account-help-text {
background: white;
border-radius: @border-radius;
border: 1px solid #cdcdcd;
box-shadow: 0 0 @space-sm rgba(100, 100, 100, .2);
display: none;
font-size: @font-size-xs;
font-weight: 400;
padding: @space-xs;
position: absolute;
width: 18rem;
z-index: 999;
ul& {
padding-left: 1.6rem;
}
li {
font-size: @font-size-xs;
font-weight: 400;
}
}
@media screen and (max-width: 767px) {
.account-help-icon li,
.account-help-icon img,
.account-help-icon p {
display: none;
}
}
// monospace things
.mono,
.form-control,
@ -283,7 +237,7 @@ input[type="password"] + .eye {
.m-addresses td:first-child {
max-width: 50px;
min-widht: 50px;
min-width: 50px;
}
.m-addresses td:last-child {
@ -444,4 +398,4 @@ label small {
.ens-response {
color: @gray;
}
}

View File

@ -1,7 +1,7 @@
@font-face {
font-family: 'Lato';
src: url('../fonts/Lato-Light.woff') format('woff'),
url('../fonts/Lato-Light.ttf') format('truetype');
url('../fonts/Lato-Light.ttf') format('truetype');
font-style: normal;
font-weight: 300;
}
@ -9,7 +9,7 @@
@font-face {
font-family: 'Lato';
src: url('../fonts/Lato-Regular.woff') format('woff'),
url('../fonts/Lato-Regular.ttf') format('truetype');
url('../fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: 400;
}
@ -17,8 +17,8 @@
@font-face {
font-family: 'Lato';
src: url('../fonts/Lato-Bold.woff') format('woff'),
url('../fonts/Lato-Bold.ttf') format('truetype');
url('../fonts/Lato-Bold.ttf') format('truetype');
font-style: normal;
font-weight: 700;
}
}

View File

@ -87,6 +87,8 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
});
} else if (ratesError) {
valuesEl = <h5>{ratesError}</h5>;
} else if (tokenBalances && tokenBalances.length === 0) {
valuesEl = <h5>No tokens found!</h5>;
} else {
valuesEl = (
<div className="EquivalentValues-values-loader">

View File

@ -8,7 +8,7 @@
padding-bottom: $space-sm;
display: flex;
flex-direction: column;
justify-content: space-between;
justify-content: space-around;
text-align: center;
align-items: center;
@ -18,26 +18,74 @@
align-items: flex-start;
}
& > div {
padding: 8px 16px;
}
&-social-media-wrap {
margin-top: 32px;
& .Footer-social-media-link {
transition: opacity 0.3s;
color: white;
margin: 1rem;
margin-left: 0;
&:hover {
opacity: 0.8;
color: white;
}
&:focus {
opacity: 0.8;
color: white;
}
}
}
&-affiliate-wrap {
& .Footer-affiliate-tag {
background-color: #0e97c0;
display: inline-block;
padding: 4px 12px;
border-radius: 30px;
margin: 0rem 0.5rem 0.5rem 0px;
transition: color 0.3s, background-color 0.3s;
&:hover {
background-color: white;
color: #0e97c0;
}
& a {
transition: color 0s;
color: inherit;
&:hover {
color: inherit;
}
}
}
}
&-column {
padding: 1rem 2rem;
}
&-about {
max-width: 22rem;
&-logo {
width: 100%;
height: auto;
max-width: 20rem;
}
&-text {
max-width: 50ch;
}
}
&-about,
&-links,
&-info {
max-width: 34rem;
}
&-links {
max-width: 28rem;
& > a {
display: block;
font-size: 0.9rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}
&-modal-button {
@ -58,10 +106,14 @@
margin: $space-xs 0 $space-sm;
}
a {
color: #4ac8ed;
a,
.Footer-modal-button {
color: #7fe5ff;
font-weight: 400;
transition: color 0.3s;
&:hover,
&:focus {
opacity: 1;
color: darken(#4ac8ed, 5%);
}
}
@ -81,7 +133,7 @@
ul {
list-style: none;
padding-left: $space-sm;
padding-left: 0;
margin: 0 0 $space-xs 0;
}
@ -101,4 +153,4 @@
.Modal {
color: #000;
}
}

View File

@ -1,100 +1,105 @@
import logo from 'assets/images/logo-myetherwallet.svg';
import { bityReferralURL, donationAddressMap } from 'config/data';
import {
bityReferralURL,
ledgerReferralURL,
trezorReferralURL,
bitboxReferralURL,
donationAddressMap
} from 'config/data';
import React from 'react';
import translate from 'translations';
import './index.scss';
import PreFooter from './PreFooter';
import Modal, { IButton } from 'components/ui/Modal';
import { NewTabLink } from 'components/ui';
const LINKS_LEFT = [
const AffiliateTag = ({ link, text }) => {
return (
<li className="Footer-affiliate-tag" key={link}>
<NewTabLink href={link}>{text}</NewTabLink>
</li>
);
};
const SocialMediaLink = ({ link, text }) => {
return (
<NewTabLink className="Footer-social-media-link" key={link} href={link}>
<i className={`sm-icon sm-logo-${text} sm-24px`} />
</NewTabLink>
);
};
const SOCIAL_MEDIA: Link[] = [
{
text: 'Knowledge Base',
href: 'https://myetherwallet.groovehq.com/help_center'
link: 'https://myetherwallet.herokuapp.com/',
text: 'slack'
},
{
text: 'Helpers & ENS Debugging',
href: 'https://www.myetherwallet.com/helpers.html'
link: 'https://www.reddit.com/r/MyEtherWallet/',
text: 'reddit'
},
{
text: 'Sign Message',
href: 'https://www.myetherwallet.com/signmsg.html'
link: 'https://twitter.com/myetherwallet',
text: 'twitter'
},
{
link: 'https://www.facebook.com/MyEtherWallet',
text: 'facebook'
},
{
link: 'https://medium.com/@myetherwallet',
text: 'medium'
},
{
link: 'https://www.linkedin.com/company/myetherwallet/',
text: 'linkedin'
},
{
link: 'https://github.com/MyEtherWallet',
text: 'github'
}
];
const LINKS_SUPPORT = [
const PRODUCT_INFO: Link[] = [
{
href: bityReferralURL,
text: 'Swap ETH/BTC/EUR/CHF via Bity.com'
},
{
href: 'https://www.ledgerwallet.com/r/fa4b?path=/products/',
text: 'Buy a Ledger Nano S'
},
{
href: 'https://trezor.io/?a=myetherwallet.com',
text: 'Buy a TREZOR'
},
{
href: 'https://digitalbitbox.com/?ref=mew',
text: 'Buy a Digital Bitbox'
}
];
const LINKS_RIGHT = [
{
href: 'https://www.MyEtherWallet.com',
text: 'MyEtherWallet.com'
},
{
href: 'https://github.com/MyEtherWallet/MyEtherWallet',
link: 'https://github.com/MyEtherWallet/MyEtherWallet',
text: 'Github: Current Site'
},
{
href: 'https://github.com/MyEtherWallet',
link: 'https://github.com/MyEtherWallet',
text: 'Github: MEW Org'
},
{
href: 'https://github.com/MyEtherWallet/MyEtherWallet/releases/latest',
link: 'https://github.com/MyEtherWallet/MyEtherWallet/releases/latest',
text: 'Github: Latest Release'
},
{
href:
link:
'https://chrome.google.com/webstore/detail/myetherwallet-cx/nlbmnnijcnlegkjjpcfjclmcfggfefdm?hl=en',
text: 'MyEtherWallet CX'
text: 'MyEtherWallet Extension'
},
{
href:
link:
'https://chrome.google.com/webstore/detail/etheraddresslookup/pdknmigbbbhmllnmgdfalmedcmcefdfn',
text: 'Anti-Phishing CX'
text: 'Anti - Phishing Extension'
}
];
const LINKS_SOCIAL = [
{
href: 'https://myetherwallet.herokuapp.com/',
text: 'Slack'
},
{
href: 'https://www.reddit.com/r/MyEtherWallet/',
text: 'Reddit'
},
{
href: 'https://twitter.com/myetherwallet',
text: 'Twitter'
},
{
href: 'https://www.facebook.com/MyEtherWallet/',
text: 'Facebook'
},
{
href: 'https://medium.com/@myetherwallet',
text: 'Medium'
}
];
interface Link {
link: string;
text: string;
}
interface Props {
latestBlock: string;
};
}
interface State {
isOpen: boolean;
@ -122,9 +127,9 @@ export default class Footer extends React.Component<Props, State> {
<div>
<PreFooter />
<footer className="Footer" role="contentinfo" aria-label="footer">
<div className="Footer-column Footer-about">
<div className="Footer-about">
<p aria-hidden="true">
<a href="/">
<NewTabLink href="/">
<img
className="Footer-about-logo"
src={logo}
@ -132,22 +137,15 @@ export default class Footer extends React.Component<Props, State> {
width="auto"
alt="MyEtherWallet"
/>
</a>
</NewTabLink>
</p>
<p className="Footer-about-text">
<span>{translate('FOOTER_1')}</span>
<span>{translate('FOOTER_1b')}</span>
</p>
{LINKS_LEFT.map(link => {
return (
<p key={link.href}>
<a href={link.href} target="_blank" rel="noopener">
{link.text}
</a>
</p>
);
})}
<p className="Footer-about-text">{translate('FOOTER_1')}</p>
<NewTabLink href="https://myetherwallet.github.io/knowledge-base">
Knowledge Base
</NewTabLink>
<NewTabLink href="https://www.myetherwallet.com/helpers.html">
Helpers & ENS Debugging
</NewTabLink>
<button className="Footer-modal-button" onClick={this.openModal}>
Disclaimer
@ -160,11 +158,11 @@ export default class Footer extends React.Component<Props, State> {
>
<p>
<b>Be safe & secure: </b>
<a href="https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds">
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds">
We highly recommend that you read our guide on How to Prevent
Loss & Theft for some recommendations on how to be proactive
about your security.
</a>
</NewTabLink>
</p>
<p>
<b>Always backup your keys: </b>
@ -220,36 +218,44 @@ export default class Footer extends React.Component<Props, State> {
OTHER DEALINGS IN THE SOFTWARE.
</b>
</Modal>
<p>Latest Block#: {this.props.latestBlock}</p>
<p>&copy; 2017 MyEtherWallet, LLC</p>
</div>
<div className="Footer-column Footer-info">
<div className="Footer-info">
<h5>
<i aria-hidden="true">👫</i>
You can support us by supporting our blockchain-family.
</h5>
<p>Consider using our affiliate links to...</p>
<ul>
{LINKS_SUPPORT.map(link => {
return (
<li key={link.href}>
<a href={link.href} target="_blank">
{link.text}
</a>
</li>
);
})}
<p>Consider using our affiliate links to</p>
<ul className="Footer-affiliate-wrap">
<AffiliateTag
link={bityReferralURL}
text="Swap ETH/BTC/EUR/CHF via Bity.com"
/>
</ul>
<p>Buy a</p>
<ul className="Footer-affiliate-wrap">
<AffiliateTag link={ledgerReferralURL} text="Ledger Nano S" />
<AffiliateTag link={trezorReferralURL} text="TREZOR" />
<AffiliateTag link={bitboxReferralURL} text="Digital Bitbox" />
</ul>
<h5>
<i aria-hidden="true">💝</i>
{translate('FOOTER_2')}
</h5>
<ul>
<li>
{' '}
ETH: <span className="mono wrap">{donationAddressMap.ETH}</span>
ETH: mewtopia.eth{' '}
<span className="mono wrap">
<NewTabLink
href={`https://etherscan.io/address/${
donationAddressMap.ETH
}`}
>
{donationAddressMap.ETH}
</NewTabLink>
</span>
</li>
<li>
{' '}
@ -258,29 +264,22 @@ export default class Footer extends React.Component<Props, State> {
</ul>
</div>
<div className="Footer-column Footer-links">
{LINKS_RIGHT.map(link => {
return (
<p key={link.href}>
<a href={link.href} target="_blank">
{link.text}
</a>
</p>
);
})}
<p>
{LINKS_SOCIAL.map((link, i) => {
return (
<span key={link.href}>
<a key={link.href} href={link.href} target="_blank">
{link.text}
</a>
{i !== LINKS_SOCIAL.length - 1 && ' · '}
</span>
);
})}
</p>
<p>Latest Block#: {this.props.latestBlock}</p>
<div className="Footer-links">
{PRODUCT_INFO.map((productInfoItem, idx) => (
<NewTabLink target="_blank" key={idx} href={productInfoItem.link}>
{productInfoItem.text}
</NewTabLink>
))}
<div className="Footer-social-media-wrap">
{SOCIAL_MEDIA.map((socialMediaItem, idx) => (
<SocialMediaLink
link={socialMediaItem.link}
key={idx}
text={socialMediaItem.text}
/>
))}
</div>
</div>
</footer>
</div>

View File

@ -2,9 +2,12 @@ import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { NETWORKS, CustomNodeConfig } from 'config/data';
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
const NETWORK_KEYS = Object.keys(NETWORKS);
const CUSTOM = 'custom';
interface Input {
name: string;
@ -13,7 +16,10 @@ interface Input {
}
interface Props {
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
handleAddCustomNode(node: CustomNodeConfig): void;
handleAddCustomNetwork(node: CustomNetworkConfig): void;
handleClose(): void;
}
@ -22,6 +28,9 @@ interface State {
url: string;
port: string;
network: string;
customNetworkName: string;
customNetworkUnit: string;
customNetworkChainId: string;
hasAuth: boolean;
username: string;
password: string;
@ -33,25 +42,34 @@ export default class CustomNodeModal extends React.Component<Props, State> {
url: '',
port: '',
network: NETWORK_KEYS[0],
customNetworkName: '',
customNetworkUnit: '',
customNetworkChainId: '',
hasAuth: false,
username: '',
password: '',
password: ''
};
public render() {
const { handleClose } = this.props;
const { customNetworks, handleClose } = this.props;
const { network } = this.state;
const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids();
const buttons: IButton[] = [{
type: 'primary',
text: translate('NODE_CTA'),
onClick: this.saveAndAdd,
disabled: !!Object.keys(invalids).length,
}, {
text: translate('x_Cancel'),
onClick: handleClose
}];
const buttons: IButton[] = [
{
type: 'primary',
text: translate('NODE_CTA'),
onClick: this.saveAndAdd,
disabled: !!Object.keys(invalids).length
},
{
text: translate('x_Cancel'),
onClick: handleClose
}
];
const conflictedNode = this.getConflictedNode();
return (
<Modal
@ -61,54 +79,119 @@ export default class CustomNodeModal extends React.Component<Props, State> {
handleClose={handleClose}
>
<div>
{isHttps &&
<div className="alert alert-danger small">
{isHttps && (
<div className="alert alert-warning small">
{translate('NODE_Warning')}
</div>
}
)}
{conflictedNode && (
<div className="alert alert-warning small">
You already have a node called '{conflictedNode.name}' that
matches this one, saving this will overwrite it
</div>
)}
<form>
<div className="row">
<div className="col-sm-7">
<label>{translate('NODE_Name')}</label>
{this.renderInput({
name: 'name',
placeholder: 'My Node',
}, invalids)}
{this.renderInput(
{
name: 'name',
placeholder: 'My Node'
},
invalids
)}
</div>
<div className="col-sm-5">
<label>Network</label>
<select
className="form-control"
name="network"
value={this.state.network}
value={network}
onChange={this.handleChange}
>
{NETWORK_KEYS.map((net) =>
<option key={net} value={net}>{net}</option>
)}
{NETWORK_KEYS.map(net => (
<option key={net} value={net}>
{net}
</option>
))}
{customNetworks.map(net => {
const id = makeCustomNetworkId(net);
return (
<option key={id} value={id}>
{net.name} (Custom)
</option>
);
})}
<option value={CUSTOM}>Custom...</option>
</select>
</div>
</div>
{network === CUSTOM && (
<div className="row">
<div className="col-sm-6">
<label className="is-required">Network Name</label>
{this.renderInput(
{
name: 'customNetworkName',
placeholder: 'My Custom Network'
},
invalids
)}
</div>
<div className="col-sm-3">
<label className="is-required">Currency</label>
{this.renderInput(
{
name: 'customNetworkUnit',
placeholder: 'ETH'
},
invalids
)}
</div>
<div className="col-sm-3">
<label>Chain ID</label>
{this.renderInput(
{
name: 'customNetworkChainId',
placeholder: 'e.g. 1'
},
invalids
)}
</div>
</div>
)}
<hr />
<div className="row">
<div className="col-sm-9">
<label>URL</label>
{this.renderInput({
name: 'url',
placeholder: 'http://127.0.0.1/',
}, invalids)}
{this.renderInput(
{
name: 'url',
placeholder: 'http://127.0.0.1/'
},
invalids
)}
</div>
<div className="col-sm-3">
<label>{translate('NODE_Port')}</label>
{this.renderInput({
name: 'port',
placeholder: '8545',
type: 'number',
}, invalids)}
{this.renderInput(
{
name: 'port',
placeholder: '8545',
type: 'number'
},
invalids
)}
</div>
</div>
<div className="row">
<div className="col-sm-12">
<label>
@ -117,27 +200,29 @@ export default class CustomNodeModal extends React.Component<Props, State> {
name="hasAuth"
checked={this.state.hasAuth}
onChange={this.handleCheckbox}
/>
{' '}
/>{' '}
<span>HTTP Basic Authentication</span>
</label>
</div>
</div>
{this.state.hasAuth &&
{this.state.hasAuth && (
<div className="row">
<div className="col-sm-6">
<label>Username</label>
<label className="is-required">Username</label>
{this.renderInput({ name: 'username' }, invalids)}
</div>
<div className="col-sm-6">
<label>Password</label>
{this.renderInput({
name: 'password',
type: 'password',
}, invalids)}
<label className="is-required">Password</label>
{this.renderInput(
{
name: 'password',
type: 'password'
},
invalids
)}
</div>
</div>
}
)}
</form>
</div>
</Modal>
@ -145,15 +230,17 @@ export default class CustomNodeModal extends React.Component<Props, State> {
}
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
return <input
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name],
})}
value={this.state[name]}
onChange={this.handleChange}
{...input}
/>;
return (
<input
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name]
})}
value={this.state[name]}
onChange={this.handleChange}
{...input}
/>
);
}
private getInvalids(): { [key: string]: boolean } {
@ -163,12 +250,16 @@ export default class CustomNodeModal extends React.Component<Props, State> {
hasAuth,
username,
password,
network,
customNetworkName,
customNetworkUnit,
customNetworkChainId
} = this.state;
const required = ["name", "url", "port", "network"];
const required = ['name', 'url', 'port', 'network'];
const invalids: { [key: string]: boolean } = {};
// Required fields
required.forEach((field) => {
required.forEach(field => {
if (!this.state[field]) {
invalids[field] = true;
}
@ -195,12 +286,67 @@ export default class CustomNodeModal extends React.Component<Props, State> {
}
}
// If they have a custom network, make sure info is provided
if (network === CUSTOM) {
if (!customNetworkName) {
invalids.customNetworkName = true;
}
if (!customNetworkUnit) {
invalids.customNetworkUnit = true;
}
// Numeric chain ID (if provided)
const iChainId = parseInt(customNetworkChainId, 10);
if (!iChainId || iChainId < 0) {
invalids.customNetworkChainId = true;
}
}
return invalids;
}
private handleChange = (ev: React.FormEvent<
HTMLInputElement | HTMLSelectElement
>) => {
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
return {
name: this.state.customNetworkName,
unit: this.state.customNetworkUnit,
chainId: this.state.customNetworkChainId
? parseInt(this.state.customNetworkChainId, 10)
: 0
};
}
private makeCustomNodeConfigFromState(): CustomNodeConfig {
const { network } = this.state;
const node: CustomNodeConfig = {
name: this.state.name.trim(),
url: this.state.url.trim(),
port: parseInt(this.state.port, 10),
network:
network === CUSTOM
? makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
: network
};
if (this.state.hasAuth) {
node.auth = {
username: this.state.username,
password: this.state.password
};
}
return node;
}
private getConflictedNode(): CustomNodeConfig | undefined {
const { customNodes } = this.props;
const config = this.makeCustomNodeConfigFromState();
const thisId = makeCustomNodeId(config);
return customNodes.find(conf => makeCustomNodeId(conf) === thisId);
}
private handleChange = (
ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = ev.currentTarget;
this.setState({ [name as any]: value });
};
@ -211,18 +357,11 @@ export default class CustomNodeModal extends React.Component<Props, State> {
};
private saveAndAdd = () => {
const node: CustomNodeConfig = {
name: this.state.name.trim(),
url: this.state.url.trim(),
port: parseInt(this.state.port, 10),
network: this.state.network,
};
const node = this.makeCustomNodeConfigFromState();
if (this.state.hasAuth) {
node.auth = {
username: this.state.username,
password: this.state.password,
};
if (this.state.network === CUSTOM) {
const network = this.makeCustomNetworkConfigFromState();
this.props.handleAddCustomNetwork(network);
}
this.props.handleAddCustomNode(node);

View File

@ -8,6 +8,7 @@ const tabs = [
name: 'NAV_GenerateWallet',
to: '/'
},
{
name: 'NAV_SendEther',
to: 'send-transaction'
@ -36,6 +37,10 @@ const tabs = [
name: 'Broadcast Transaction',
to: 'pushTx'
},
{
name: 'NAV_Utilities',
to: 'utilities'
},
{
name: 'NAV_Help',
to: 'https://myetherwallet.groovehq.com/help_center',

View File

@ -3,7 +3,8 @@ import {
TChangeLanguage,
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode
TRemoveCustomNode,
TAddCustomNetwork
} from 'actions/config';
import logo from 'assets/images/logo-myetherwallet.svg';
import { Dropdown, ColorDropdown } from 'components/ui';
@ -14,17 +15,18 @@ import {
ANNOUNCEMENT_MESSAGE,
ANNOUNCEMENT_TYPE,
languages,
NETWORKS,
NODES,
VERSION,
NodeConfig,
CustomNodeConfig
} from '../../config/data';
CustomNodeConfig,
CustomNetworkConfig
} from 'config/data';
import GasPriceDropdown from './components/GasPriceDropdown';
import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal';
import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network';
import './index.scss';
interface Props {
@ -34,12 +36,14 @@ interface Props {
isChangingNode: boolean;
gasPriceGwei: number;
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
changeGasPrice: TChangeGasPrice;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
addCustomNetwork: TAddCustomNetwork;
}
interface State {
@ -58,40 +62,47 @@ export default class Header extends Component<Props, State> {
node,
nodeSelection,
isChangingNode,
customNodes
customNodes,
customNetworks
} = this.props;
const { isAddingCustomNode } = this.state;
const selectedLanguage = languageSelection;
const selectedNetwork = NETWORKS[node.network];
const selectedNetwork = getNetworkConfigFromId(
node.network,
customNetworks
);
const LanguageDropDown = Dropdown as new () => Dropdown<
typeof selectedLanguage
>;
const nodeOptions = Object.keys(NODES)
.map(key => {
const n = NODES[key];
const network = getNetworkConfigFromId(n.network, customNetworks);
return {
value: key,
name: (
<span>
{NODES[key].network} <small>({NODES[key].service})</small>
{network && network.name} <small>({n.service})</small>
</span>
),
color: NETWORKS[NODES[key].network].color,
hidden: NODES[key].hidden
color: network && network.color,
hidden: n.hidden
};
})
.concat(
customNodes.map(customNode => {
customNodes.map(cn => {
const network = getNetworkConfigFromId(cn.network, customNetworks);
return {
value: makeCustomNodeId(customNode),
value: makeCustomNodeId(cn),
name: (
<span>
{customNode.network} - {customNode.name} <small>(custom)</small>
{network && network.name} - {cn.name} <small>(custom)</small>
</span>
),
color: '#000',
color: network && network.color,
hidden: false,
onRemove: () => this.props.removeCustomNode(customNode)
onRemove: () => this.props.removeCustomNode(cn)
};
})
);
@ -161,8 +172,8 @@ export default class Header extends Component<Props, State> {
>
<ColorDropdown
ariaLabel={`
change node. current node ${node.network}
node by ${node.service}
change node. current node is on the ${node.network} network
provided by ${node.service}
`}
options={nodeOptions}
value={nodeSelection}
@ -182,11 +193,14 @@ export default class Header extends Component<Props, State> {
</section>
</section>
<Navigation color={selectedNetwork.color} />
<Navigation color={selectedNetwork && selectedNetwork.color} />
{isAddingCustomNode && (
<CustomNodeModal
customNodes={customNodes}
customNetworks={customNetworks}
handleAddCustomNode={this.addCustomNode}
handleAddCustomNetwork={this.props.addCustomNetwork}
handleClose={this.closeCustomNodeModal}
/>
)}

View File

@ -3,6 +3,7 @@ import { IFullWallet } from 'ethereumjs-wallet';
import React from 'react';
import translate from 'translations';
import printElement from 'utils/printElement';
import { stripHexPrefix } from 'libs/values';
const print = (address: string, privateKey: string) => () =>
address &&
@ -27,7 +28,7 @@ const print = (address: string, privateKey: string) => () =>
const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
const address = wallet.getAddressString();
const privateKey = wallet.getPrivateKeyString();
const privateKey = stripHexPrefix(wallet.getPrivateKeyString());
if (!address || !privateKey) {
return null;

View File

@ -1,4 +1,5 @@
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
@ -8,13 +9,6 @@ export interface PrivateKeyValue {
valid: boolean;
}
function fixPkey(key) {
if (key.indexOf('0x') === 0) {
return key.slice(2);
}
return key;
}
interface Validated {
fixedPkey: string;
isValidPkey: boolean;
@ -23,7 +17,7 @@ interface Validated {
}
function validatePkeyAndPass(pkey: string, pass: string): Validated {
const fixedPkey = fixPkey(pkey);
const fixedPkey = stripHexPrefix(pkey);
const validPkey = isValidPrivKey(fixedPkey);
const validEncPkey = isValidEncryptedPrivKey(fixedPkey);
const isValidPkey = validPkey || validEncPkey;
@ -58,15 +52,13 @@ export default class PrivateKeyDecrypt extends Component {
return (
<section className="col-md-4 col-sm-6">
<div id="selectedTypeKey">
<h4>
{translate('ADD_Radio_3')}
</h4>
<h4>{translate('ADD_Radio_3')}</h4>
<div className="form-group">
<textarea
id="aria-private-key"
className={`form-control ${isValidPkey
? 'is-valid'
: 'is-invalid'}`}
className={`form-control ${
isValidPkey ? 'is-valid' : 'is-invalid'
}`}
value={key}
onChange={this.onPkeyChange}
onKeyDown={this.onKeyDown}
@ -75,22 +67,21 @@ export default class PrivateKeyDecrypt extends Component {
/>
</div>
{isValidPkey &&
isPassRequired &&
<div className="form-group">
<p>
{translate('ADD_Label_3')}
</p>
<input
className={`form-control ${password.length > 0
? 'is-valid'
: 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>}
isPassRequired && (
<div className="form-group">
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${
password.length > 0 ? 'is-valid' : 'is-invalid'
}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
)}
</div>
</section>
);

View File

@ -1,29 +1,60 @@
import React, { Component } from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config/data';
import { isValidETHAddress } from 'libs/validators';
import { AddressOnlyWallet } from 'libs/wallet';
interface Props {
onUnlock(param: any): void;
}
interface State {
address: string;
}
export default class ViewOnlyDecrypt extends Component<Props, State> {
public state = {
address: ''
};
export default class ViewOnlyDecrypt extends Component {
public render() {
const { address } = this.state;
const isValid = isValidETHAddress(address);
return (
<section className="col-md-4 col-sm-6">
<div id="selectedUploadKey">
<h4>
{translate('ADD_Radio_2_alt')}
</h4>
<h4>{translate('MYWAL_Address')}</h4>
<div className="form-group">
<input type="file" id="fselector" />
<form className="form-group" onSubmit={this.openWallet}>
<input
className={`form-control
${isValid ? 'is-valid' : 'is-invalid'}
`}
onChange={this.changeAddress}
value={address}
placeholder={donationAddressMap.ETH}
/>
<a
className="btn-file marg-v-sm"
id="aria1"
tabIndex={0}
role="button"
>
{translate('ADD_Radio_2_short')}
</a>
</div>
<button className="btn btn-primary btn-block" disabled={!isValid}>
{translate('NAV_ViewWallet')}
</button>
</form>
</div>
</section>
);
}
private changeAddress = (ev: React.FormEvent<HTMLInputElement>) => {
this.setState({ address: ev.currentTarget.value });
};
private openWallet = (ev: React.SyntheticEvent<HTMLFormElement>) => {
const { address } = this.state;
ev.preventDefault();
if (isValidETHAddress(address)) {
const wallet = new AddressOnlyWallet(address);
this.props.onUnlock(wallet);
}
};
}

View File

@ -13,7 +13,7 @@ export default class Web3Decrypt extends Component<Props> {
<section className="Web3Decrypt col-md-4 col-sm-6">
<div>
<button
className="Web3Decrypt btn btn-primary btn-lg"
className="Web3Decrypt-decrypt btn btn-primary btn-lg"
onClick={this.props.onUnlock}
>
{translate('ADD_MetaMask')}

View File

@ -22,8 +22,32 @@ import TrezorDecrypt from './Trezor';
import ViewOnlyDecrypt from './ViewOnly';
import { AppState } from 'reducers';
import Web3Decrypt from './Web3';
import Help from 'components/ui/Help';
const WALLETS = {
web3: {
lid: 'x_MetaMask',
component: Web3Decrypt,
initialParams: {},
unlock: unlockWeb3,
helpLink:
'https://myetherwallet.github.io/knowledge-base/migration/moving-from-private-key-to-metamask.html'
},
'ledger-nano-s': {
lid: 'x_Ledger',
component: LedgerNanoSDecrypt,
initialParams: {},
unlock: setWallet,
helpLink:
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
},
trezor: {
lid: 'x_Trezor',
component: TrezorDecrypt,
initialParams: {},
unlock: setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
},
'keystore-file': {
lid: 'x_Keystore2',
component: KeystoreDecrypt,
@ -32,7 +56,16 @@ const WALLETS = {
password: ''
},
unlock: unlockKeystore,
disabled: false
helpLink:
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
initialParams: {},
unlock: unlockMnemonic,
helpLink:
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
},
'private-key': {
lid: 'x_PrivKey2',
@ -42,40 +75,15 @@ const WALLETS = {
password: ''
},
unlock: unlockPrivateKey,
disabled: false
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
initialParams: {},
unlock: unlockMnemonic,
disabled: false
},
'ledger-nano-s': {
lid: 'x_Ledger',
component: LedgerNanoSDecrypt,
initialParams: {},
unlock: setWallet,
disabled: false
},
trezor: {
lid: 'x_Trezor',
component: TrezorDecrypt,
initialParams: {},
unlock: setWallet,
disabled: false
},
web3: {
lid: 'x_MetaMask',
component: Web3Decrypt,
initialParams: {},
unlock: unlockWeb3,
disabled: false
helpLink:
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
},
'view-only': {
lid: 'View with Address Only',
component: ViewOnlyDecrypt,
disabled: true
initialParams: {},
unlock: setWallet,
helpLink: ''
}
};
@ -87,6 +95,7 @@ interface Props {
UnlockKeystoreAction | UnlockMnemonicAction | UnlockPrivateKeyAction
>;
offline: boolean;
allowReadOnly?: boolean;
}
interface State {
@ -126,7 +135,11 @@ export class WalletDecrypt extends Component<Props, State> {
public buildWalletOptions() {
return map(WALLETS, (wallet, key) => {
const { helpLink } = wallet;
const isSelected = this.state.selectedWalletKey === key;
const isDisabled =
this.isOnlineRequiredWalletAndOffline(key) ||
(!this.props.allowReadOnly && wallet.component === ViewOnlyDecrypt);
return (
<label className="radio" key={key}>
@ -137,12 +150,11 @@ export class WalletDecrypt extends Component<Props, State> {
name="decryption-choice-radio-group"
value={key}
checked={isSelected}
disabled={isDisabled}
onChange={this.handleDecryptionChoiceChange}
disabled={
wallet.disabled || this.isOnlineRequiredWalletAndOffline(key)
}
/>
<span id={`${key}-label`}>{translate(wallet.lid)}</span>
{helpLink ? <Help link={helpLink} /> : null}
</label>
);
});

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { IWallet, IFullWallet } from 'libs/wallet';
interface Props {
wallet: IWallet;
withFullWallet(wallet: IFullWallet): React.ReactElement<any>;
withoutFullWallet(): React.ReactElement<any>;
}
class FullWalletOnly extends React.Component<Props, {}> {
public render() {
const { wallet, withFullWallet, withoutFullWallet } = this.props;
if (!wallet || wallet.isReadOnly) {
if (withoutFullWallet) {
return withoutFullWallet();
}
return null;
}
return withFullWallet(wallet);
}
}
export default connect((state: AppState) => ({
wallet: state.wallet.inst
}))(FullWalletOnly);

View File

@ -0,0 +1,30 @@
.Help {
display: inline-block;
margin-left: 8px;
padding: 4px;
box-sizing: content-box;
line-height: inherit;
vertical-align: top;
transition: opacity 0.3s;
& img {
display: block;
height: inherit;
width: inherit;
}
&-x1 {
height: 16px;
width: 16px;
}
&-x2 {
height: 24px;
width: 24px;
}
&-x3 {
height: 36px;
width: 36px;
}
}

View File

@ -1,36 +1,18 @@
import React from 'react';
import helpIcon from 'assets/images/icon-help.svg';
import icon from 'assets/images/icon-help-3.svg';
import './Help.scss';
type sizeType = 'small' | 'medium' | 'large';
type Size = 'x1' | 'x2' | 'x3';
interface HelpProps {
interface Props {
link: string;
size?: sizeType;
helpText?: string;
size?: Size;
}
const Help = ({ size, link, helpText }: HelpProps) => {
let width = 30;
let height = 12;
switch (size) {
case 'medium':
width = width * 3;
height = height * 3;
break;
case 'large':
width = width * 4;
height = height * 4;
break;
default:
break;
}
const Help = ({ size = 'x1', link }: Props) => {
return (
<a href={link} className={'account-help-icon'} target={'_blank'}>
<img src={helpIcon} width={width} height={height} />
{helpText && <p className="account-help-text">{helpText}</p>}
<a href={link} className={`Help Help-${size}`} target={'_blank'}>
<img src={icon} />
</a>
);
};

View File

@ -1,4 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import {
fromTokenBase,
getDecimal,
@ -7,6 +8,9 @@ import {
TokenValue
} from 'libs/units';
import { formatNumber as format } from 'utils/formatters';
import Spinner from 'components/ui/Spinner';
import { getOffline } from 'selectors/config';
import { AppState } from 'reducers';
interface Props {
/**
@ -41,33 +45,69 @@ const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance } = params;
let element;
if (!value) {
return <span>Balance isn't available offline</span>;
}
const convertedValue = isEthereumUnit(params)
? fromTokenBase(value, getDecimal(params.unit))
: fromTokenBase(value, params.decimal);
let formattedValue;
if (displayShortBalance) {
const digits =
typeof displayShortBalance === 'number' && displayShortBalance;
formattedValue = digits
? format(convertedValue, digits)
: format(convertedValue);
element = <Spinner size="x1" />;
} else {
formattedValue = convertedValue;
const convertedValue = isEthereumUnit(params)
? fromTokenBase(value, getDecimal(params.unit))
: fromTokenBase(value, params.decimal);
let formattedValue;
if (displayShortBalance) {
const digits =
typeof displayShortBalance === 'number' ? displayShortBalance : 4;
formattedValue = format(convertedValue, digits);
// If the formatted value was too low, display something like < 0.01
if (
parseFloat(formattedValue) === 0 &&
parseFloat(convertedValue) !== 0
) {
const padding = digits !== 0 ? `.${'0'.repeat(digits - 1)}1` : '';
formattedValue = `< 0${padding}`;
}
} else {
formattedValue = convertedValue;
}
element = (
<span>
{formattedValue}
{symbol ? ` ${symbol}` : ''}
</span>
);
}
return (
<span>
{formattedValue}
{symbol ? ` ${symbol}` : ''}
</span>
);
return <ConnectedOfflineDisplay>{element}</ConnectedOfflineDisplay>;
};
export default UnitDisplay;
/**
* @description Helper component for displaying alternate text when offline.
* Circumvents typescript issue with union props on connected components.
*/
interface OfflineProps {
offline: AppState['config']['offline'];
children: React.ReactElement<string>;
}
class OfflineDisplay extends React.Component<OfflineProps> {
public render() {
if (this.props.offline) {
return <span>Balance isn't available offline</span>;
} else {
return this.props.children;
}
}
}
function mapStateToOfflineProps(state: AppState) {
return {
offline: getOffline(state)
};
}
const ConnectedOfflineDisplay = connect(mapStateToOfflineProps)(OfflineDisplay);

View File

@ -7,6 +7,7 @@ import { AppState } from 'reducers';
interface Props {
title: React.ReactElement<any>;
wallet: IWallet;
allowReadOnly?: boolean;
}
interface State {
expanded: boolean;
@ -28,7 +29,7 @@ export class UnlockHeader extends React.Component<Props, State> {
}
public render() {
const { title } = this.props;
const { title, allowReadOnly } = this.props;
return (
<article className="collapse-container">
<div>
@ -37,7 +38,7 @@ export class UnlockHeader extends React.Component<Props, State> {
</a>
<h1>{title}</h1>
</div>
{this.state.expanded && <WalletDecrypt />}
{this.state.expanded && <WalletDecrypt allowReadOnly={allowReadOnly} />}
{this.state.expanded && <hr />}
</article>
);

View File

@ -2,7 +2,8 @@ import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
export const languages = require('./languages.json');
// Displays in the header
export const VERSION = '4.0.0 (Alpha 0.0.4)';
export const VERSION = '4.0.0 (Alpha 0.0.5)';
export const N_FACTOR = 1024;
// Displays at the top of the site, make message empty string to remove.
// Type can be primary, warning, danger, success, or info.
@ -40,6 +41,10 @@ export const gasPriceDefaults = {
};
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
export const ledgerReferralURL =
'https://www.ledgerwallet.com/r/fa4b?path=/products/';
export const trezorReferralURL = 'https://trezor.io/?a=myetherwallet.com';
export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mew';
export interface BlockExplorerConfig {
name: string;
@ -51,6 +56,7 @@ export interface Token {
address: string;
symbol: string;
decimal: number;
error?: string | null;
}
export interface NetworkContract {
@ -73,6 +79,12 @@ export interface NetworkConfig {
contracts: NetworkContract[] | null;
}
export interface CustomNetworkConfig {
name: string;
unit: string;
chainId: number;
}
export interface NodeConfig {
network: string;
lib: RPCNode | Web3Node;
@ -256,43 +268,34 @@ export const NODES: { [key: string]: NodeConfig } = {
}
};
export function initWeb3Node(): Promise<void> {
return new Promise((resolve, reject) => {
const { web3 } = window as any;
export async function initWeb3Node(): Promise<void> {
const { web3 } = window as any;
if (!web3) {
return reject(
new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
)
);
}
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
throw new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
);
}
if (web3.version.network === 'loading') {
return reject(
new Error(
'MetaMask / Mist is still loading. Please refresh the page and try again.'
)
);
}
const lib = new Web3Node();
const networkId = await lib.getNetVersion();
const accounts = await lib.getAccounts();
web3.version.getNetwork((err, networkId) => {
if (err) {
return reject(err);
}
if (!accounts.length) {
throw new Error('No accounts found in MetaMask / Mist.');
}
try {
NODES.web3 = {
network: networkIdToName(networkId),
service: 'MetaMask / Mist',
lib: new Web3Node(web3),
estimateGas: false,
hidden: true
};
resolve();
} catch (err) {
reject(err);
}
});
});
if (networkId === 'loading') {
throw new Error(
'MetaMask / Mist is still loading. Please refresh the page and try again.'
);
}
NODES.web3 = {
network: networkIdToName(networkId),
service: 'MetaMask / Mist',
lib,
estimateGas: false,
hidden: true
};
}

View File

@ -1,40 +1,49 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
changeGasPrice as dChangeGasPrice,
changeLanguage as dChangeLanguage,
changeNodeIntent as dChangeNodeIntent,
addCustomNode as dAddCustomNode,
removeCustomNode as dRemoveCustomNode,
addCustomNetwork as dAddCustomNetwork,
TChangeGasPrice,
TChangeLanguage,
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork
} from 'actions/config';
import { AlphaAgreement, Footer, Header } from 'components';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import Notifications from './Notifications';
import { NodeConfig, CustomNodeConfig } from 'config/data';
interface Props {
// FIXME
children: any;
languageSelection: string;
node: NodeConfig;
nodeSelection: string;
isChangingNode: boolean;
gasPriceGwei: number;
customNodes: CustomNodeConfig[];
latestBlock: string;
interface ReduxProps {
languageSelection: AppState['config']['languageSelection'];
node: AppState['config']['node'];
nodeSelection: AppState['config']['nodeSelection'];
isChangingNode: AppState['config']['isChangingNode'];
gasPriceGwei: AppState['config']['gasPriceGwei'];
customNodes: AppState['config']['customNodes'];
customNetworks: AppState['config']['customNetworks'];
latestBlock: AppState['config']['latestBlock'];
}
interface ActionProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
changeGasPrice: TChangeGasPrice;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
addCustomNetwork: TAddCustomNetwork;
}
type Props = {
// FIXME
children: any;
} & ReduxProps &
ActionProps;
class TabSection extends Component<Props, {}> {
public render() {
const {
@ -46,6 +55,7 @@ class TabSection extends Component<Props, {}> {
languageSelection,
gasPriceGwei,
customNodes,
customNetworks,
latestBlock,
changeLanguage,
@ -53,6 +63,7 @@ class TabSection extends Component<Props, {}> {
changeGasPrice,
addCustomNode,
removeCustomNode,
addCustomNetwork
} = this.props;
const headerProps = {
@ -62,12 +73,14 @@ class TabSection extends Component<Props, {}> {
isChangingNode,
gasPriceGwei,
customNodes,
customNetworks,
changeLanguage,
changeNodeIntent,
changeGasPrice,
addCustomNode,
removeCustomNode,
addCustomNetwork
};
return (
@ -84,7 +97,7 @@ class TabSection extends Component<Props, {}> {
}
}
function mapStateToProps(state: AppState) {
function mapStateToProps(state: AppState): ReduxProps {
return {
node: state.config.node,
nodeSelection: state.config.nodeSelection,
@ -92,7 +105,8 @@ function mapStateToProps(state: AppState) {
languageSelection: state.config.languageSelection,
gasPriceGwei: state.config.gasPriceGwei,
customNodes: state.config.customNodes,
latestBlock: state.config.latestBlock,
customNetworks: state.config.customNetworks,
latestBlock: state.config.latestBlock
};
}
@ -102,4 +116,5 @@ export default connect(mapStateToProps, {
changeNodeIntent: dChangeNodeIntent,
addCustomNode: dAddCustomNode,
removeCustomNode: dRemoveCustomNode,
addCustomNetwork: dAddCustomNetwork
})(TabSection);

View File

@ -127,6 +127,10 @@ export const deployHOC = PassedComponent => {
value
};
if (!props.wallet || props.wallet.isReadOnly) {
return;
}
return makeAndSignTx(
props.wallet,
props.nodeLib,
@ -139,6 +143,10 @@ export const deployHOC = PassedComponent => {
};
private getAddressAndNonce = async () => {
if (!this.props.wallet || this.props.wallet.isReadOnly) {
return;
}
const address = await this.props.wallet.getAddressString();
const nonce = await this.props.nodeLib
.getTransactionCount(address)

View File

@ -1,12 +1,13 @@
import { Wei } from 'libs/units';
import { IWallet, Balance } from 'libs/wallet';
import { Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
import { TBroadcastTx } from 'actions/wallet';
import { TShowNotification } from 'actions/notifications';
import { AppState } from 'reducers';
export interface Props {
wallet: IWallet;
wallet: AppState['wallet']['inst'];
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;

View File

@ -4,12 +4,12 @@ import { toWei, Wei, getDecimal } from 'libs/units';
import { connect } from 'react-redux';
import { showNotification, TShowNotification } from 'actions/notifications';
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
import { IWallet, Balance } from 'libs/wallet';
import { IFullWallet, Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
export interface IWithTx {
wallet: IWallet;
wallet: IFullWallet;
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;
@ -20,18 +20,21 @@ export interface IWithTx {
showNotification: TShowNotification;
}
const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst,
balance: state.wallet.balance,
node: configSelectors.getNodeConfig(state),
nodeLib: configSelectors.getNodeLib(state),
chainId: configSelectors.getNetworkConfig(state).chainId,
networkName: configSelectors.getNetworkConfig(state).name,
gasPrice: toWei(
`${configSelectors.getGasPriceGwei(state)}`,
getDecimal('gwei')
)
});
const mapStateToProps = (state: AppState) => {
const network = configSelectors.getNetworkConfig(state);
return {
wallet: state.wallet.inst,
balance: state.wallet.balance,
node: configSelectors.getNodeConfig(state),
nodeLib: configSelectors.getNodeLib(state),
chainId: network ? network.chainId : 0,
networkName: network ? network.name : 'Unknown network',
gasPrice: toWei(
`${configSelectors.getGasPriceGwei(state)}`,
getDecimal('gwei')
)
};
};
export const withTx = passedComponent =>
connect(mapStateToProps, {

View File

@ -7,6 +7,7 @@ import translate from 'translations';
import { makeBlob } from 'utils/blob';
import './DownloadWallet.scss';
import Template from './Template';
import { N_FACTOR } from 'config/data';
interface Props {
wallet: IFullWallet;
@ -47,7 +48,7 @@ export default class DownloadWallet extends Component<Props, State> {
role="button"
className="DlWallet-download btn btn-primary btn-lg"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby="x_KeystoreDesc"
aria-describedby={translate('x_KeystoreDesc')}
download={filename}
href={this.getBlob()}
onClick={this.handleDownloadKeystore}
@ -130,7 +131,7 @@ export default class DownloadWallet extends Component<Props, State> {
this.state.hasDownloadedWallet && this.props.continueToPaper();
private setWallet(wallet: IFullWallet, password: string) {
const keystore = wallet.toV3(password, { n: 1024 });
const keystore = wallet.toV3(password, { n: N_FACTOR });
keystore.address = toChecksumAddress(keystore.address);
this.setState({ keystore });
}

View File

@ -1,41 +1,37 @@
import { GenerateNewWalletAction } from 'actions/generateWallet';
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Field, reduxForm } from 'redux-form';
import translate from 'translations';
import './EnterPassword.scss';
import PasswordInput from './PasswordInput';
import Template from './Template';
// VALIDATORS
const minLength = min => value => {
return value && value.length < min
? `Must be ${min} characters or more`
: undefined;
};
const minLength = min => value => value && value.length >= min;
const minLength9 = minLength(9);
const required = value => (value ? undefined : 'Required');
interface Props {
walletPasswordForm: any;
generateNewWallet(pw: string): GenerateNewWalletAction;
}
interface State {
fileName: null | string;
blobURI: null | string;
password: string;
isPasswordValid: boolean;
isPasswordVisible: boolean;
}
class EnterPassword extends Component<Props, State> {
export default class EnterPassword extends Component<Props, State> {
public state = {
fileName: null,
blobURI: null,
password: '',
isPasswordValid: false,
isPasswordVisible: false
};
public render() {
const { walletPasswordForm } = this.props;
const { isPasswordVisible } = this.state;
const AnyField = Field as new () => Field<any>;
const { password, isPasswordValid, isPasswordVisible } = this.state;
const content = (
<div className="EnterPw">
<h1 className="EnterPw-title" aria-live="polite">
@ -44,20 +40,18 @@ class EnterPassword extends Component<Props, State> {
<label className="EnterPw-password">
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
<AnyField
className="EnterPw-password-field"
validate={[required, minLength9]}
component={PasswordInput}
<PasswordInput
password={password}
onPasswordChange={this.onPasswordChange}
isPasswordVisible={isPasswordVisible}
togglePassword={this.togglePassword}
name="password"
type="text"
isPasswordValid={isPasswordValid}
/>
</label>
<button
onClick={this.onClickGenerateFile}
disabled={walletPasswordForm ? walletPasswordForm.syncErrors : true}
disabled={!isPasswordValid}
className="EnterPw-submit btn btn-primary btn-block"
>
{translate('NAV_GenerateWallet')}
@ -131,15 +125,19 @@ class EnterPassword extends Component<Props, State> {
return <Template content={content} help={help} />;
}
private onClickGenerateFile = () => {
const form = this.props.walletPasswordForm;
this.props.generateNewWallet(form.values.password);
this.props.generateNewWallet(this.state.password);
this.setState({ password: '' });
};
private togglePassword = () => {
this.setState({ isPasswordVisible: !this.state.isPasswordVisible });
};
}
export default reduxForm({
form: 'walletPasswordForm' // a unique name for this form
})(EnterPassword as any);
private onPasswordChange = (e: any) => {
const password = e.target.value;
this.setState({
isPasswordValid: minLength9(password),
password
});
};
}

View File

@ -4,6 +4,7 @@ import { NewTabLink } from 'components/ui';
import React from 'react';
import { Link } from 'react-router-dom';
import translate from 'translations';
import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss';
import Template from './Template';
@ -13,7 +14,7 @@ const content = (wallet: IFullWallet) => (
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<input
className="GenPaper-private form-control"
value={wallet.getPrivateKeyString()}
value={stripHexPrefix(wallet.getPrivateKeyString())}
aria-label={translate('x_PrivKey')}
aria-describedby="x_PrivKeyDesc"
type="text"

View File

@ -2,27 +2,35 @@ import React, { Component } from 'react';
import { translateRaw } from 'translations';
interface Props {
password: string;
onPasswordChange: any;
togglePassword: any;
isPasswordVisible?: boolean;
input: any;
meta: any;
isPasswordValid: boolean;
}
export default class PasswordInput extends Component<Props, {}> {
public render() {
const { input, meta, isPasswordVisible, togglePassword } = this.props;
const {
password,
isPasswordValid,
isPasswordVisible,
togglePassword,
onPasswordChange
} = this.props;
return (
<div>
<div>
<div className="input-group" style={{ width: '100%' }}>
<input
{...input}
value={password}
name="password"
className={`form-control ${meta.error ? 'is-invalid' : ''}`}
className={`form-control ${!isPasswordValid ? 'is-invalid' : ''}`}
type={isPasswordVisible ? 'text' : 'password'}
placeholder={translateRaw('GEN_Placeholder_1')}
aria-label={translateRaw('GEN_Aria_1')}
onChange={onPasswordChange}
/>
<span
onClick={togglePassword}

View File

@ -21,7 +21,6 @@ interface Props {
activeStep: string; // FIXME union actual steps
password: string;
wallet: IFullWallet | null | undefined;
walletPasswordForm: any;
// Actions
generateNewWallet: TGenerateNewWallet;
continueToPaper: TContinueToPaper;
@ -44,7 +43,6 @@ class GenerateWallet extends Component<Props, {}> {
case 'password':
content = (
<AnyEnterPassword
walletPasswordForm={this.props.walletPasswordForm}
generateNewWallet={this.props.generateNewWallet}
/>
);
@ -87,7 +85,6 @@ class GenerateWallet extends Component<Props, {}> {
function mapStateToProps(state: AppState) {
return {
walletPasswordForm: state.form.walletPasswordForm,
activeStep: state.generateWallet.activeStep,
password: state.generateWallet.password,
wallet: state.generateWallet.wallet

View File

@ -0,0 +1,23 @@
@import 'common/sass/variables';
.KeystoreDetails {
&-title {
margin: $space auto $space * 2.5;
}
&-password,
&-key {
max-width: 40rem;
margin: 0 auto $space;
&-label {
margin-bottom: $space;
}
}
&-submit,
&-download {
max-width: 16rem;
margin: 0 auto $space * 3;
}
}

View File

@ -0,0 +1,177 @@
import React, { Component } from 'react';
import Template from './Template';
import KeystoreInput from './KeystoreInput';
import { fromPrivateKey, IFullWallet, fromV3 } from 'ethereumjs-wallet';
import { makeBlob } from 'utils/blob';
import { isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import translate from 'translations';
import './KeystoreDetails.scss';
import { N_FACTOR } from 'config/data';
interface State {
secretKey: string;
password: string;
fileName: string;
isPasswordVisible: boolean;
isPrivateKeyVisible: boolean;
wallet: IFullWallet | null | undefined;
}
const initialState: State = {
secretKey: '',
password: '',
isPasswordVisible: false,
isPrivateKeyVisible: false,
fileName: '',
wallet: null
};
const minLength = min => value => value && value.length >= min;
const minLength9 = minLength(9);
class KeystoreDetails extends Component<{}, State> {
public state = initialState;
public componentWillUnmount() {
this.resetState();
}
public render() {
const {
secretKey,
isPasswordVisible,
isPrivateKeyVisible,
password,
wallet,
fileName
} = this.state;
const privateKey = stripHexPrefix(secretKey);
const privateKeyValid = isValidPrivKey(privateKey);
const content = (
<div className="KeystoreDetails">
<div>
<label className="KeystoreDetails-key">
<h4 className="KeystoreDetails-label">Private Key</h4>
<KeystoreInput
isValid={privateKeyValid}
isVisible={isPrivateKeyVisible}
name="secretKey"
value={secretKey}
handleInput={this.handleInput}
placeholder="Enter your saved private key here"
handleToggle={this.togglePrivateKey}
/>
</label>
</div>
<div>
<label className="KeystoreDetails-password">
<h4 className="KeystoreDetails-label">Password</h4>
<KeystoreInput
isValid={minLength9(password)}
isVisible={isPasswordVisible}
name="password"
value={password}
placeholder="Enter your encryption password here."
handleInput={this.handleInput}
handleToggle={this.togglePassword}
/>
</label>
</div>
{!wallet ? (
<button
onClick={this.handleKeystoreGeneration}
className="KeystoreDetails-submit btn btn-primary btn-block"
disabled={!privateKeyValid || !minLength9(password)}
>
Generate Keystore
</button>
) : this.runtimeKeystoreCheck() ? (
<a
onClick={this.resetState}
href={this.getBlob()}
className="KeystoreDetails-download btn btn-success btn-block"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby={translate('x_KeystoreDesc')}
download={fileName}
>
Download Keystore
</a>
) : (
<p>
Error generating a valid keystore that matches your private key. In
order to protect our users, if our runtime check fails, we prevent
you from downloading a potentially corrupted wallet.
</p>
)}
</div>
);
return (
<div>
<Template title="Regenerate Keystore File" content={content} />
</div>
);
}
private togglePrivateKey = () => {
this.setState({
isPrivateKeyVisible: !this.state.isPrivateKeyVisible
});
};
private togglePassword = () => {
this.setState({
isPasswordVisible: !this.state.isPasswordVisible
});
};
private resetState = () => {
this.setState(initialState);
};
private handleKeystoreGeneration = () => {
const { secretKey } = this.state;
const removeChecksumPkey = stripHexPrefix(secretKey);
const keyBuffer = Buffer.from(removeChecksumPkey, 'hex');
const wallet = fromPrivateKey(keyBuffer);
const fileName = wallet.getV3Filename();
this.setState({
wallet,
fileName
});
};
private handleInput = (e: React.FormEvent<HTMLInputElement>) => {
const name = e.currentTarget.name;
const value = e.currentTarget.value;
if (name === 'secretKey') {
this.setState({
wallet: null
});
}
this.setState({ [name as any]: value });
};
private runtimeKeystoreCheck(): boolean {
const { wallet, password, secretKey } = this.state;
if (wallet) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
const backToWallet = fromV3(keystore, password, true);
if (stripHexPrefix(backToWallet.getPrivateKeyString()) === secretKey) {
return true;
}
}
return false;
}
private getBlob() {
const { wallet, password } = this.state;
if (wallet) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
return makeBlob('text/json;charset=UTF-8', keystore);
}
}
}
export default KeystoreDetails;

View File

@ -0,0 +1,43 @@
import React from 'react';
import classnames from 'classnames';
interface Props {
isValid: boolean;
isVisible: boolean;
name: string;
value: string;
placeholder: string;
handleInput(e: React.FormEvent<HTMLInputElement>): void;
handleToggle(): void;
}
const KeystoreInput: React.SFC<Props> = ({
isValid,
isVisible,
handleInput,
name,
value,
placeholder,
handleToggle
}) => (
<div className="input-group">
<input
className={classnames(
'form-control',
isValid ? 'is-valid' : 'is-invalid'
)}
type={isVisible ? 'text' : 'password'}
name={name}
placeholder={placeholder}
value={value}
onChange={handleInput}
/>
<span
onClick={handleToggle}
role="button"
className="input-group-addon eye"
/>
</div>
);
export default KeystoreInput;

View File

@ -0,0 +1,16 @@
import React from 'react';
interface Props {
content: React.ReactElement<any>;
title: string;
}
const RestoreKeystoreTemplate: React.SFC<Props> = ({ title, content }) => (
<div className="Tab-content">
<div className="Tab-content-pane text-center">
<h1>{title}</h1>
{content}
</div>
</div>
);
export default RestoreKeystoreTemplate;

View File

@ -0,0 +1,11 @@
import React from 'react';
import TabSection from 'containers/TabSection';
import KeystoreDetails from './components/KeystoreDetails';
const RestoreKeystore: React.SFC<{}> = () => (
<TabSection>
<KeystoreDetails />
</TabSection>
);
export default RestoreKeystore;

View File

@ -1,13 +1,7 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import Modal, { IButton } from 'components/ui/Modal';
import { Location, History as H } from 'history';
type UnregisterCallback = () => void;
type BooleanCallback = (arg?: any) => boolean;
interface History extends H {
block(prompt?: boolean | BooleanCallback): UnregisterCallback;
}
import { Location, History } from 'history';
interface Props {
when: boolean;
@ -50,7 +44,9 @@ class NavigationPrompt extends React.Component<Props, State> {
nextLocation
});
}
return !this.props.when;
if (this.props.when) {
return false;
}
});
}

View File

@ -27,17 +27,16 @@ export default class NonceField extends React.Component<PublicProps, {}> {
return (
<div className="row form-group">
<div className="col-xs-11">
<label>Nonce</label>
<Help
size={'small'}
link={
'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'
}
/>
<label>Nonce</label>
<input
className={`form-control ${isValidNonce(strValue)
? 'is-valid'
: 'is-invalid'}`}
className={`form-control ${
isValidNonce(strValue) ? 'is-valid' : 'is-invalid'
}`}
type="number"
value={strValue}
placeholder={placeholder}

View File

@ -32,7 +32,13 @@ import {
import { UnitKey, Wei, getDecimal, toWei } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
// LIBS
import { IWallet, Balance, Web3Wallet } from 'libs/wallet';
import {
IWallet,
Balance,
Web3Wallet,
LedgerWallet,
TrezorWallet
} from 'libs/wallet';
import pickBy from 'lodash/pickBy';
import React from 'react';
// REDUX
@ -265,6 +271,8 @@ export class SendTransaction extends React.Component<Props, State> {
? getDecimal('ether')
: (this.state.token && this.state.token.decimal) || 0;
const isWeb3Wallet = this.props.wallet instanceof Web3Wallet;
const isLedgerWallet = this.props.wallet instanceof LedgerWallet;
const isTrezorWallet = this.props.wallet instanceof TrezorWallet;
return (
<TabSection>
<section className="Tab-content">
@ -277,6 +285,7 @@ export class SendTransaction extends React.Component<Props, State> {
) : null}
</div>
}
allowReadOnly={true}
/>
<NavigationPrompt
when={unlocked}
@ -284,8 +293,9 @@ export class SendTransaction extends React.Component<Props, State> {
/>
<div className="row">
{/* Send Form */}
{unlocked &&
!(offline || (forceOffline && isWeb3Wallet)) && (
!((offline || forceOffline) && isWeb3Wallet) && (
<main className="col-sm-8">
<div className="Tab-content-pane">
{hasQueryString && (
@ -353,7 +363,16 @@ export class SendTransaction extends React.Component<Props, State> {
{generateTxProcessing && (
<div className="container">
<div className="row form-group text-center">
<Spinner size="x5" />
{isLedgerWallet || isTrezorWallet ? (
<div>
<p>
<b>Confirm transaction on hardware wallet</b>
</p>
<Spinner size="x2" />
</div>
) : (
<Spinner size="x2" />
)}
</div>
</div>
)}
@ -416,7 +435,7 @@ export class SendTransaction extends React.Component<Props, State> {
)}
{unlocked &&
(offline || (forceOffline && isWeb3Wallet)) && (
((offline || forceOffline) && isWeb3Wallet) && (
<main className="col-sm-8">
<div className="Tab-content-pane">
<h4>Sorry...</h4>
@ -493,6 +512,10 @@ export class SendTransaction extends React.Component<Props, State> {
public async getFormattedTxFromState(): Promise<TransactionWithoutGas> {
const { wallet } = this.props;
if (wallet.isReadOnly) {
throw new Error('Wallet is read-only');
}
const { token, unit, value, to, data } = this.state;
const transactionInput: TransactionInput = {
token,
@ -675,6 +698,7 @@ export class SendTransaction extends React.Component<Props, State> {
await this.resetJustTx();
const { nodeLib, wallet, gasPrice, network, offline } = this.props;
const { token, unit, value, to, data, gasLimit, nonce } = this.state;
const chainId = network.chainId;
const transactionInput = {
token,
@ -685,6 +709,10 @@ export class SendTransaction extends React.Component<Props, State> {
};
const bigGasLimit = Wei(gasLimit);
try {
if (wallet.isReadOnly) {
throw new Error('Wallet is read-only');
}
const signedTx = await generateCompleteTransaction(
wallet,
nodeLib,

View File

@ -0,0 +1,46 @@
import React from 'react';
import translate from 'translations';
import { ISignedMessage } from 'libs/signing';
import { IFullWallet } from 'libs/wallet';
import { TShowNotification } from 'actions/notifications';
interface Props {
wallet: IFullWallet;
message: string;
showNotification: TShowNotification;
onSignMessage(msg: ISignedMessage): any;
}
export default class SignMessageButton extends React.Component<Props, {}> {
public render() {
return (
<button
className="SignMessage-sign btn btn-primary btn-lg"
onClick={this.handleSignMessage}
>
{translate('NAV_SignMsg')}
</button>
);
}
private handleSignMessage = async () => {
const { wallet, message, showNotification, onSignMessage } = this.props;
try {
const signedMessage: ISignedMessage = {
address: await wallet.getAddressString(),
message,
signature: await wallet.signMessage(message),
version: '2'
};
onSignMessage(signedMessage);
showNotification(
'success',
`Successfully signed message with address ${signedMessage.address}.`
);
} catch (err) {
showNotification('danger', `Error signing message: ${err.message}`);
}
};
}

View File

@ -1,28 +1,26 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { IWallet } from 'libs/wallet/IWallet';
import WalletDecrypt from 'components/WalletDecrypt';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { ISignedMessage } from 'libs/signing';
import { AppState } from 'reducers';
import { IFullWallet } from 'libs/wallet';
import FullWalletOnly from 'components/renderCbs/FullWalletOnly';
import SignButton from './SignButton';
import './index.scss';
interface Props {
wallet: IWallet;
showNotification: TShowNotification;
}
interface State {
message: string;
signMessageError: string;
signedMessage: ISignedMessage | null;
}
const initialState: State = {
message: '',
signMessageError: '',
signedMessage: null
};
@ -33,7 +31,6 @@ export class SignMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { wallet } = this.props;
const { message, signedMessage } = this.state;
const messageBoxClass = classnames([
@ -56,14 +53,10 @@ export class SignMessage extends Component<Props, State> {
<div className="SignMessage-help">{translate('MSG_info2')}</div>
</div>
{!!wallet && (
<button
className="SignMessage-sign btn btn-primary btn-lg"
onClick={this.handleSignMessage}
>
{translate('NAV_SignMsg')}
</button>
)}
<FullWalletOnly
withFullWallet={this.renderSignButton}
withoutFullWallet={this.renderUnlock}
/>
{!!signedMessage && (
<div>
@ -79,53 +72,33 @@ export class SignMessage extends Component<Props, State> {
</div>
)}
</div>
{!wallet && <WalletDecrypt />}
</div>
);
}
private handleSignMessage = async () => {
const { wallet } = this.props;
const { message } = this.state;
if (!wallet) {
return;
}
try {
const signedMessage: ISignedMessage = {
address: await wallet.getAddressString(),
message,
signature: await wallet.signMessage(message),
version: '2'
};
this.setState({ signedMessage });
this.props.showNotification(
'success',
`Successfully signed message with address ${signedMessage.address}.`
);
} catch (err) {
this.props.showNotification(
'danger',
`Error signing message: ${err.message}`
);
}
};
private handleMessageChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
const message = e.currentTarget.value;
this.setState({ message });
};
}
function mapStateToProps(state: AppState) {
return {
wallet: state.wallet.inst
private onSignMessage = (signedMessage: ISignedMessage) => {
this.setState({ signedMessage });
};
private renderSignButton = (fullWallet: IFullWallet) => {
return (
<SignButton
wallet={fullWallet}
message={this.state.message}
showNotification={this.props.showNotification}
onSignMessage={this.onSignMessage}
/>
);
};
private renderUnlock() {
return <WalletDecrypt />;
}
}
export default connect(mapStateToProps, {
showNotification
})(SignMessage);
export default connect(null, { showNotification })(SignMessage);

176
common/freezer.ts Normal file
View File

@ -0,0 +1,176 @@
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
const PROJECT_BASE = path.resolve('./');
const ORACLE_BRANCH = 'develop';
const GET_PACKAGE_CMD = `git show ${ORACLE_BRANCH}:package.json`;
const GET_DIFF_CMD = `git diff origin/${ORACLE_BRANCH}`;
const newFileRegEx = /^\+\+\+ b\//;
const frozenFolderRegEx = /\/\*$/;
const start = async () => {
try {
const packageStr = await runShCommand(GET_PACKAGE_CMD);
const diff = await runShCommand(GET_DIFF_CMD);
const { frozen } = JSON.parse(packageStr);
if (frozen === undefined) {
console.log(
`Freezer: No config found in package.json on branch ${
ORACLE_BRANCH
}. Exiting.`
);
return;
}
const newFiles = getNewFiles(diff);
const frozenFiles = getFrozenFiles(frozen);
const frozenFolders = getFrozenFolders(frozen);
ensureNewFilesAreNotFrozen(newFiles, frozenFiles, frozenFolders);
} catch (err) {
console.log(err.message);
exit();
}
};
const ensureNewFilesAreNotFrozen = (
newFiles: string[],
frozenFiles: string[],
frozenFolders: string[]
): void => {
const errors = newFiles
.map(file => {
if (frozenFiles.indexOf(file) !== -1) {
return `"${file}" is frozen`;
}
if (isFileInFrozenFolders(file, frozenFolders)) {
return `"${file}" is in a frozen folder`;
}
})
.filter(err => err);
if (errors.length) {
throw new Error(`Frozen files have been modified:\n${errors.join('\n')}`);
} else {
console.log('Freezer: no frozen files modified.');
}
};
const isFileInFrozenFolders = (file: string, folders: string[]): boolean =>
folders.reduce((isFrozen, folder) => {
if (isFrozen) {
return isFrozen;
}
const folderSplit = folder.replace(frozenFolderRegEx, '').split('/');
const fileSplit = file.split('/').slice(0, folderSplit.length);
return JSON.stringify(folderSplit) === JSON.stringify(fileSplit);
}, false);
const getFrozenFiles = (frozen: string[]): string[] =>
frozen.filter(f => !frozenFolderRegEx.test(f));
const getFrozenFolders = (frozen: string[]): string[] =>
frozen.filter(f => frozenFolderRegEx.test(f));
const getNewFiles = (diff: string): string[] =>
diff
.split('\n')
.filter(line => newFileRegEx.test(line))
.map(line => line.replace(newFileRegEx, ''));
const runShCommand = (cmd: string): Promise<string> =>
new Promise((resolve, reject) => {
const sh = spawn('sh', ['-c', cmd]);
const stdout: string[] = [];
const stderr: string[] = [];
sh.stdout.on('data', data => {
stdout.push(data.toString());
});
sh.stderr.on('data', data => {
stderr.push(data.toString());
});
sh.on('close', code => {
if (code !== 0) {
console.error(stderr.join(''));
reject(`Child process closed with code ${code}`);
}
resolve(stdout.join(''));
});
});
const isTravisPushJob = () => {
const prb = process.env.TRAVIS_PULL_REQUEST_BRANCH;
return typeof prb === 'string' && prb.length === 0;
};
const exit = () => setTimeout(() => process.exit(1), 100);
// check to make sure that all of the freezer config in
// the "frozen" property of package.json is valid
const validateConfig = () => {
try {
const packagePath = path.resolve(PROJECT_BASE, 'package.json');
const { frozen } = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (frozen === undefined) {
console.log(
`Freezer: No config found in package.json on branch ${
ORACLE_BRANCH
}. Exiting.`
);
return;
}
if (!Array.isArray(frozen)) {
throw new Error(`Property "frozen" is not an array`);
}
const errors = frozen
.map(filePath => {
const isFolder = frozenFolderRegEx.test(filePath);
const fullPath = isFolder
? path.resolve(PROJECT_BASE, filePath.replace(frozenFolderRegEx, ''))
: path.resolve(PROJECT_BASE, filePath);
if (!fs.existsSync(fullPath)) {
return `"${filePath}" does not exist`;
}
const stats = fs.lstatSync(fullPath);
if (isFolder) {
if (!stats.isDirectory()) {
return `"${filePath}" is not a folder`;
}
} else {
if (!stats.isFile()) {
return `"${filePath}" is not a file`;
}
}
})
.filter(err => err);
if (errors.length) {
throw new Error(errors.join('\n'));
} else {
console.log('Freezer: Config is valid.');
}
} catch (err) {
console.log(`Freezer: Invalid config on package.json:\n${err.message}`);
exit();
}
};
if (isTravisPushJob()) {
console.log('Freezer: Travis push job detected. Exiting.');
} else if (process.argv[2] === '--validate') {
validateConfig();
} else {
start();
}

View File

@ -1,5 +1,5 @@
import AbiFunction, { IUserSendParams, ISendParams } from './ABIFunction';
import { IWallet } from 'libs/wallet/IWallet';
import { IFullWallet } from 'libs/wallet/IWallet';
import { RPCNode } from 'libs/nodes';
import { ContractOutputMappings } from './types';
import { Wei } from 'libs/units';
@ -12,7 +12,7 @@ const ABIFUNC_METHOD_NAMES = [
];
export interface ISetConfigForTx {
wallet: IWallet;
wallet: IFullWallet;
nodeLib: RPCNode;
chainId: number;
gasPrice: Wei;
@ -36,25 +36,26 @@ export default class Contract {
.setGasPrice(gasPrice);
public static getFunctions = (contract: Contract) =>
Object.getOwnPropertyNames(
contract
).reduce((accu, currContractMethodName) => {
const currContractMethod = contract[currContractMethodName];
const methodNames = Object.getOwnPropertyNames(currContractMethod);
Object.getOwnPropertyNames(contract).reduce(
(accu, currContractMethodName) => {
const currContractMethod = contract[currContractMethodName];
const methodNames = Object.getOwnPropertyNames(currContractMethod);
const isFunc = ABIFUNC_METHOD_NAMES.reduce(
(isAbiFunc, currAbiFuncMethodName) =>
isAbiFunc && methodNames.includes(currAbiFuncMethodName),
true
);
return isFunc
? { ...accu, [currContractMethodName]: currContractMethod }
: accu;
}, {});
const isFunc = ABIFUNC_METHOD_NAMES.reduce(
(isAbiFunc, currAbiFuncMethodName) =>
isAbiFunc && methodNames.includes(currAbiFuncMethodName),
true
);
return isFunc
? { ...accu, [currContractMethodName]: currContractMethod }
: accu;
},
{}
);
public address: string;
public abi;
private wallet: IWallet;
private wallet: IFullWallet;
private gasPrice: Wei;
private chainId: number;
private node: RPCNode;
@ -68,7 +69,7 @@ export default class Contract {
return this;
};
public setWallet = (w: IWallet) => {
public setWallet = (w: IFullWallet) => {
this.wallet = w;
return this;
};

View File

@ -7,9 +7,16 @@ export interface TxObj {
data: string;
}
export interface INode {
ping(): Promise<boolean>;
getBalance(address: string): Promise<Wei>;
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
getTokenBalance(
address: string,
token: Token
): Promise<{ balance: TokenValue; error: string | null }>;
getTokenBalances(
address: string,
tokens: Token[]
): Promise<{ balance: TokenValue; error: string | null }[]>;
estimateGas(tx: TransactionWithoutGas): Promise<Wei>;
getTransactionCount(address: string): Promise<string>;
sendRawTx(tx: string): Promise<string>;

View File

@ -6,7 +6,9 @@ export default class EtherscanClient extends RPCClient {
public encodeRequest(request: EtherscanRequest): string {
const encoded = new URLSearchParams();
Object.keys(request).forEach(key => {
encoded.set(key, request[key]);
if (request[key]) {
encoded.set(key, request[key]);
}
});
return encoded.toString();
}
@ -14,9 +16,9 @@ export default class EtherscanClient extends RPCClient {
public call = (request: EtherscanRequest): Promise<JsonRpcResponse> =>
fetch(this.endpoint, {
method: 'POST',
headers: {
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
}),
body: this.encodeRequest(request)
}).then(r => r.json());

View File

@ -15,7 +15,7 @@ export default class EtherscanRequests extends RPCRequests {
public sendRawTx(signedTx: string): SendRawTxRequest {
return {
module: 'proxy',
method: 'eth_sendRawTransaction',
action: 'eth_sendRawTransaction',
hex: signedTx
};
}
@ -23,7 +23,7 @@ export default class EtherscanRequests extends RPCRequests {
public estimateGas(transaction): EstimateGasRequest {
return {
module: 'proxy',
method: 'eth_estimateGas',
action: 'eth_estimateGas',
to: transaction.to,
value: transaction.value,
data: transaction.data,
@ -71,7 +71,7 @@ export default class EtherscanRequests extends RPCRequests {
public getCurrentBlock(): GetCurrentBlockRequest {
return {
module: 'proxy',
action: 'eth_blockNumber',
action: 'eth_blockNumber'
};
}
}

View File

@ -5,7 +5,7 @@ export interface EtherscanReqBase {
export interface SendRawTxRequest extends EtherscanReqBase {
module: 'proxy';
method: 'eth_sendRawTransaction';
action: 'eth_sendRawTransaction';
hex: string;
}
@ -27,7 +27,7 @@ export type GetTokenBalanceRequest = CallRequest;
export interface EstimateGasRequest extends EtherscanReqBase {
module: 'proxy';
method: 'eth_estimateGas';
action: 'eth_estimateGas';
to: string;
value: string | number;
data: string;

View File

@ -2,7 +2,7 @@ import { randomBytes } from 'crypto';
import RPCClient from '../rpc/client';
export default class InfuraClient extends RPCClient {
public id(): string {
return `0x${randomBytes(5).toString('hex')}`;
public id(): number {
return parseInt(randomBytes(5).toString('hex'), 16);
}
}

View File

@ -3,13 +3,13 @@ import { JsonRpcResponse, RPCRequest } from './types';
export default class RPCClient {
public endpoint: string;
public headers: object;
constructor(endpoint: string, headers: object = {}) {
public headers: { [key: string]: string };
constructor(endpoint: string, headers: { [key: string]: string } = {}) {
this.endpoint = endpoint;
this.headers = headers;
}
public id(): string {
public id(): string | number {
return randomBytes(16).toString('hex');
}
@ -22,10 +22,10 @@ export default class RPCClient {
public call = (request: RPCRequest | any): Promise<JsonRpcResponse> => {
return fetch(this.endpoint, {
method: 'POST',
headers: {
headers: this.createHeaders({
'Content-Type': 'application/json',
...this.headers,
},
...this.headers
}),
body: JSON.stringify(this.decorateRequest(request))
}).then(r => r.json());
};
@ -33,11 +33,19 @@ export default class RPCClient {
public batch = (requests: RPCRequest[] | any): Promise<JsonRpcResponse[]> => {
return fetch(this.endpoint, {
method: 'POST',
headers: {
headers: this.createHeaders({
'Content-Type': 'application/json',
...this.headers,
},
...this.headers
}),
body: JSON.stringify(requests.map(this.decorateRequest))
}).then(r => r.json());
};
private createHeaders = headerObject => {
const headers = new Headers();
Object.keys(headerObject).forEach(name => {
headers.append(name, headerObject[name]);
});
return headers;
};
}

View File

@ -6,13 +6,15 @@ import { stripHexPrefix } from 'libs/values';
import { INode, TxObj } from '../INode';
import RPCClient from './client';
import RPCRequests from './requests';
function errorOrResult(response) {
if (response.error) {
throw new Error(response.error.message);
}
return response.result;
}
import {
isValidGetBalance,
isValidEstimateGas,
isValidCallRequest,
isValidTokenBalance,
isValidTransactionCount,
isValidCurrentBlock,
isValidRawTxApi
} from '../../validators';
export default class RpcNode implements INode {
public client: RPCClient;
@ -23,79 +25,95 @@ export default class RpcNode implements INode {
this.requests = new RPCRequests();
}
public ping(): Promise<boolean> {
return this.client
.call(this.requests.getNetVersion())
.then(() => true)
.catch(() => false);
}
public sendCallRequest(txObj: TxObj): Promise<string> {
return this.client.call(this.requests.ethCall(txObj)).then(r => {
if (r.error) {
throw Error(r.error.message);
}
return r.result;
});
return this.client
.call(this.requests.ethCall(txObj))
.then(isValidCallRequest)
.then(response => response.result);
}
public getBalance(address: string): Promise<Wei> {
return this.client
.call(this.requests.getBalance(address))
.then(errorOrResult)
.then(result => Wei(result));
.then(isValidGetBalance)
.then(({ result }) => Wei(result));
}
public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
return this.client
.call(this.requests.estimateGas(transaction))
.then(errorOrResult)
.then(result => Wei(result));
.then(isValidEstimateGas)
.then(({ result }) => Wei(result));
}
public getTokenBalance(address: string, token: Token): Promise<TokenValue> {
public getTokenBalance(
address: string,
token: Token
): Promise<{ balance: TokenValue; error: string | null }> {
return this.client
.call(this.requests.getTokenBalance(address, token))
.then(response => {
if (response.error) {
// TODO - Error handling
return TokenValue('0');
}
return TokenValue(response.result);
});
.then(isValidTokenBalance)
.then(({ result }) => {
return {
balance: TokenValue(result),
error: null
};
})
.catch(err => ({
balance: TokenValue('0'),
error: 'Caught error:' + err
}));
}
public getTokenBalances(
address: string,
tokens: Token[]
): Promise<TokenValue[]> {
): Promise<{ balance: TokenValue; error: string | null }[]> {
return this.client
.batch(tokens.map(t => this.requests.getTokenBalance(address, t)))
.then(response => {
return response.map(item => {
// FIXME wrap in maybe-like
if (item.error) {
return TokenValue('0');
.then(response =>
response.map(item => {
if (isValidTokenBalance(item)) {
return {
balance: TokenValue(item.result),
error: null
};
} else {
return {
balance: TokenValue('0'),
error: 'Invalid object shape'
};
}
return TokenValue(item.result);
});
});
// TODO - Error handling
})
);
}
public getTransactionCount(address: string): Promise<string> {
return this.client
.call(this.requests.getTransactionCount(address))
.then(errorOrResult);
.then(isValidTransactionCount)
.then(({ result }) => result);
}
public getCurrentBlock(): Promise<string> {
return this.client
.call(this.requests.getCurrentBlock())
.then(errorOrResult)
.then(result => new BN(stripHexPrefix(result)).toString());
.then(isValidCurrentBlock)
.then(({ result }) => new BN(stripHexPrefix(result)).toString());
}
public sendRawTx(signedTx: string): Promise<string> {
return this.client
.call(this.requests.sendRawTx(signedTx))
.then(response => {
if (response.error) {
throw new Error(response.error.message);
}
return response.result;
.then(isValidRawTxApi)
.then(({ result }) => {
return result;
});
}
}

View File

@ -7,11 +7,15 @@ import {
GetTokenBalanceRequest,
GetTransactionCountRequest,
SendRawTxRequest,
GetCurrentBlockRequest,
GetCurrentBlockRequest
} from './types';
import { hexEncodeData } from './utils';
import { TxObj } from '../INode';
export default class RPCRequests {
public getNetVersion() {
return { method: 'net_version' };
}
public sendRawTx(signedTx: string): SendRawTxRequest | any {
return {
method: 'eth_sendRawTransaction',
@ -67,7 +71,7 @@ export default class RPCRequests {
public getCurrentBlock(): GetCurrentBlockRequest | any {
return {
method: 'eth_blockNumber',
method: 'eth_blockNumber'
};
}
}

View File

@ -1,8 +1,8 @@
// don't use flow temporarily
import { TransactionWithoutGas } from 'libs/messages';
type DATA = string;
type QUANTITY = string;
export type DATA = string;
export type QUANTITY = string;
type TX = string;
export type DEFAULT_BLOCK = string | 'earliest' | 'latest' | 'pending';
@ -19,8 +19,9 @@ export interface JsonRpcResponse {
};
}
interface RPCRequestBase {
export interface RPCRequestBase {
method: string;
params?: any[];
}
export interface SendRawTxRequest extends RPCRequestBase {
@ -70,10 +71,11 @@ export interface GetTransactionCountRequest extends RPCRequestBase {
}
export interface GetCurrentBlockRequest extends RPCRequestBase {
method: 'eth_blockNumber'
method: 'eth_blockNumber';
}
export type RPCRequest =
| RPCRequestBase //base added so I can add an empty params array in decorateRequest without TS complaining
| GetBalanceRequest
| GetTokenBalanceRequest
| CallRequest

View File

@ -0,0 +1,43 @@
import { JsonRpcResponse, RPCRequest } from '../rpc/types';
import { IWeb3Provider } from './types';
import RPCClient from '../rpc/client';
export default class Web3Client extends RPCClient {
private provider: IWeb3Provider;
constructor() {
super('web3'); // initialized with fake endpoint
this.provider = (window as any).web3.currentProvider;
}
public decorateRequest = (req: RPCRequest) => ({
...req,
id: this.id(),
jsonrpc: '2.0',
params: req.params || [] // default to empty array so MetaMask doesn't error
});
public call = (request: RPCRequest | any): Promise<JsonRpcResponse> =>
this.sendAsync(this.decorateRequest(request)) as Promise<JsonRpcResponse>;
public batch = (requests: RPCRequest[] | any): Promise<JsonRpcResponse[]> =>
this.sendAsync(requests.map(this.decorateRequest)) as Promise<
JsonRpcResponse[]
>;
private sendAsync = (
request: any
): Promise<JsonRpcResponse | JsonRpcResponse[]> => {
return new Promise((resolve, reject) => {
this.provider.sendAsync(
request,
(error, result: JsonRpcResponse | JsonRpcResponse[]) => {
if (error) {
return reject(error);
}
resolve(result);
}
);
});
};
}

View File

@ -1,151 +1,55 @@
import { Token } from 'config/data';
import { TransactionWithoutGas } from 'libs/messages';
import { Wei, TokenValue } from 'libs/units';
import { INode, TxObj } from '../INode';
import ERC20 from 'libs/erc20';
import RPCNode from '../rpc';
import Web3Client from './client';
import Web3Requests from './requests';
import { Web3Transaction } from './types';
import { INode } from 'libs/nodes/INode';
export default class Web3Node implements INode {
private web3: any;
import {
isValidSendTransaction,
isValidSignMessage,
isValidGetAccounts,
isValidGetNetVersion
} from '../../validators';
constructor(web3: any) {
this.web3 = web3;
export default class Web3Node extends RPCNode {
public client: Web3Client;
public requests: Web3Requests;
constructor() {
super('web3'); // initialized with fake endpoint
this.client = new Web3Client();
this.requests = new Web3Requests();
}
public sendCallRequest(txObj: TxObj): Promise<string> {
return new Promise((resolve, reject) => {
this.web3.eth.call(txObj, 'pending', (err, res) => {
if (err) {
return reject(err.message);
}
// web3 return string
resolve(res);
});
});
public getNetVersion(): Promise<string> {
return this.client
.call(this.requests.getNetVersion())
.then(isValidGetNetVersion)
.then(({ result }) => result);
}
public getBalance(address: string): Promise<Wei> {
return new Promise((resolve, reject) => {
this.web3.eth.getBalance(address, (err, res) => {
if (err) {
return reject(err);
}
// web3 returns BigNumber
resolve(Wei(res.toString()));
});
});
public sendTransaction(web3Tx: Web3Transaction): Promise<string> {
return this.client
.call(this.requests.sendTransaction(web3Tx))
.then(isValidSendTransaction)
.then(({ result }) => result);
}
public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
return new Promise((resolve, reject) =>
this.web3.eth.estimateGas(
{
to: transaction.to,
data: transaction.data
},
(err, res) => {
if (err) {
return reject(err);
}
// web3 returns number
resolve(Wei(res));
}
)
);
public signMessage(msgHex: string, fromAddr: string): Promise<string> {
return this.client
.call(this.requests.signMessage(msgHex, fromAddr))
.then(isValidSignMessage)
.then(({ result }) => result);
}
public getTokenBalance(address: string, token: Token): Promise<TokenValue> {
return new Promise(resolve => {
this.web3.eth.call(
{
to: token.address,
data: ERC20.balanceOf(address)
},
'pending',
(err, res) => {
if (err) {
// TODO - Error handling
return resolve(TokenValue('0'));
}
// web3 returns string
resolve(TokenValue(res));
}
);
});
}
public getTokenBalances(
address: string,
tokens: Token[]
): Promise<TokenValue[]> {
return new Promise(resolve => {
const batch = this.web3.createBatch();
const totalCount = tokens.length;
const returnArr = new Array<TokenValue>(totalCount);
let finishCount = 0;
tokens.forEach((token, index) =>
batch.add(
this.web3.eth.call.request(
{
to: token.address,
data: ERC20.balanceOf(address)
},
'pending',
(err, res) => finish(index, err, res)
)
)
);
batch.execute();
function finish(index, err, res) {
if (err) {
// TODO - Error handling
returnArr[index] = TokenValue('0');
} else {
// web3 returns string
returnArr[index] = TokenValue(res);
}
finishCount++;
if (finishCount === totalCount) {
resolve(returnArr);
}
}
});
}
public getTransactionCount(address: string): Promise<string> {
return new Promise((resolve, reject) =>
this.web3.eth.getTransactionCount(address, 'pending', (err, txCount) => {
if (err) {
return reject(err);
}
// web3 returns number
resolve(txCount.toString());
})
);
}
public getCurrentBlock(): Promise<string> {
return new Promise((resolve, reject) =>
this.web3.eth.getBlock('latest', false, (err, block) => {
if (err) {
return reject(err);
}
resolve(block.number);
})
);
}
public sendRawTx(signedTx: string): Promise<string> {
return new Promise((resolve, reject) =>
this.web3.eth.sendRawTransaction(signedTx, (err, txHash) => {
if (err) {
return reject(err);
}
// web3 return string
resolve(txHash);
})
);
public getAccounts(): Promise<string> {
return this.client
.call(this.requests.getAccounts())
.then(isValidGetAccounts)
.then(({ result }) => result);
}
}
export function isWeb3Node(nodeLib: INode | Web3Node): nodeLib is Web3Node {
return nodeLib instanceof Web3Node;
}

View File

@ -0,0 +1,29 @@
import RPCRequests from '../rpc/requests';
import {
SendTransactionRequest,
SignMessageRequest,
GetAccountsRequest,
Web3Transaction
} from './types';
export default class Web3Requests extends RPCRequests {
public sendTransaction(web3Tx: Web3Transaction): SendTransactionRequest {
return {
method: 'eth_sendTransaction',
params: [web3Tx]
};
}
public signMessage(msgHex: string, fromAddr: string): SignMessageRequest {
return {
method: 'personal_sign',
params: [msgHex, fromAddr]
};
}
public getAccounts(): GetAccountsRequest {
return {
method: 'eth_accounts'
};
}
}

View File

@ -0,0 +1,57 @@
import {
JsonRpcResponse,
RPCRequest,
RPCRequestBase,
DATA,
QUANTITY
} from '../rpc/types';
type MESSAGE_HEX = string;
type ADDRESS = string;
export interface Web3Transaction {
from: string;
to: string;
value: string;
gas: string;
gasPrice: string;
data: string;
nonce: string;
}
export interface SendTransactionRequest extends RPCRequestBase {
method: 'eth_sendTransaction';
params: [
{
from: DATA;
to: DATA;
gas: QUANTITY;
gasPrice: QUANTITY;
value: QUANTITY;
data?: DATA;
nonce?: QUANTITY;
}
];
}
export interface SignMessageRequest extends RPCRequestBase {
method: 'personal_sign';
params: [MESSAGE_HEX, ADDRESS];
}
export interface GetAccountsRequest extends RPCRequestBase {
method: 'eth_accounts';
}
type TWeb3ProviderCallback = (
error,
result: JsonRpcResponse | JsonRpcResponse[]
) => any;
type TSendAsync = (
request: RPCRequest | any,
callback: TWeb3ProviderCallback
) => void;
export interface IWeb3Provider {
sendAsync: TSendAsync;
}

View File

@ -8,7 +8,7 @@ import { INode } from 'libs/nodes/INode';
import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
import { stripHexPrefixAndLower, sanitizeHex, toHexWei } from 'libs/values';
import { IWallet, Web3Wallet } from 'libs/wallet';
import { IFullWallet, Web3Wallet } from 'libs/wallet';
import { translateRaw } from 'translations';
export interface TransactionInput {
@ -168,7 +168,7 @@ function generateTxValidation(
export async function generateCompleteTransactionFromRawTransaction(
node: INode,
tx: ExtendedRawTransaction,
wallet: IWallet,
wallet: IFullWallet,
token: Token | null | undefined,
skipValidation: boolean,
offline?: boolean
@ -198,7 +198,7 @@ export async function generateCompleteTransactionFromRawTransaction(
to: toChecksumAddress(cleanHex(to)),
value: token ? '0x00' : cleanHex(value.toString(16)),
data: data ? cleanHex(data) : '',
chainId: chainId || 1
chainId: chainId || 0
};
// Sign the transaction
@ -213,7 +213,7 @@ export async function generateCompleteTransactionFromRawTransaction(
}
export async function formatTxInput(
wallet: IWallet,
wallet: IFullWallet,
{ token, unit, value, to, data }: TransactionInput
): Promise<TransactionWithoutGas> {
if (unit === 'ether') {
@ -265,7 +265,7 @@ export async function confirmAndSendWeb3Transaction(
}
export async function generateCompleteTransaction(
wallet: IWallet,
wallet: IFullWallet,
nodeLib: RPCNode,
gasPrice: Wei,
gasLimit: Wei,

View File

@ -1,312 +0,0 @@
import { Token } from 'config/data';
import EthTx from 'ethereumjs-tx';
import { addHexPrefix, padToEven, toChecksumAddress } from 'ethereumjs-util';
import ERC20 from 'libs/erc20';
import { TransactionWithoutGas } from 'libs/messages';
import { RPCNode } from 'libs/nodes';
import { INode } from 'libs/nodes/INode';
import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
import { stripHexPrefixAndLower, toHexWei, sanitizeHex } from 'libs/values';
import { IWallet } from 'libs/wallet';
import { translateRaw } from 'translations';
export interface TransactionInput {
token?: Token | null;
unit: UnitKey;
value: string;
to: string;
data: string;
}
export interface BroadcastTransactionStatus {
isBroadcasting: boolean;
signedTx: string;
successfullyBroadcast: boolean;
}
export interface BaseTransaction {
to: string;
value: string;
data: string;
gasLimit: Wei | string;
gasPrice: Wei | string;
chainId: number;
}
export interface RawTransaction extends BaseTransaction {
nonce: string;
}
export interface ExtendedRawTransaction extends RawTransaction {
// non-standard, legacy
from: string;
}
export interface CompleteTransaction extends RawTransaction {
rawTx: string;
signedTx: string;
}
// Get useable fields from an EthTx object.
export function getTransactionFields(tx: EthTx) {
// For some crazy reason, toJSON spits out an array, not keyed values.
const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = tx.toJSON();
return {
// No value comes back as '0x', but most things expect '0x00'
value: value === '0x' ? '0x00' : value,
// If data is 0x, it might as well not be there
data: data === '0x' ? null : data,
// To address is unchecksummed, which could cause mismatches in comparisons
to: toChecksumAddress(to),
from: sanitizeHex(tx.getSenderAddress().toString('hex')),
// Everything else is as-is
nonce,
gasPrice,
gasLimit,
v,
r,
s
};
}
function getValue(
token: Token | null | undefined,
tx: ExtendedRawTransaction
): Wei {
let value;
if (token) {
value = Wei(ERC20.$transfer(tx.data).value);
} else {
value = Wei(tx.value);
}
return value;
}
async function getBalance(
node: INode,
tx: ExtendedRawTransaction,
token: Token | null | undefined
) {
const { from } = tx;
const ETHBalance = await node.getBalance(from);
let balance: Wei;
if (token) {
balance = toTokenBase(
await node.getTokenBalance(tx.from, token).toString(),
token.decimal
);
} else {
balance = ETHBalance;
}
return {
balance,
ETHBalance
};
}
async function balanceCheck(
node: INode,
tx: ExtendedRawTransaction,
token: Token | null | undefined,
value: Wei,
gasCost: Wei
) {
// Ensure their balance exceeds the amount they're sending
const { balance, ETHBalance } = await getBalance(node, tx, token);
if (value.gt(balance)) {
throw new Error(translateRaw('GETH_Balance'));
}
// ensure gas cost is not greaterThan current eth balance
// TODO check that eth balance is not lesser than txAmount + gasCost
if (gasCost.gt(ETHBalance)) {
throw new Error(
`gasCost: ${gasCost.toString()} greaterThan ETHBalance: ${ETHBalance.toString()}`
);
}
}
function generateTxValidation(
to: string,
token: Token | null | undefined,
data: string,
gasLimit: Wei | string,
gasPrice: Wei | string,
skipEthAddressValidation: boolean
) {
// Reject bad addresses
if (!isValidETHAddress(to) && !skipEthAddressValidation) {
throw new Error(translateRaw('ERROR_5'));
}
// Reject token transactions without data
if (token && !data) {
throw new Error('Tokens must be sent with data');
}
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
throw Error('Gas Limit and Gas Price should be of type bignumber');
}
// Reject gas limit under 21000 (Minimum for transaction)
// Reject if limit over 5000000
// TODO: Make this dynamic, the limit shifts
if (gasLimit.ltn(21000)) {
throw new Error('Gas limit must be at least 21000 for transactions');
}
// Reject gasLimit over 5000000gwei
if (gasLimit.gtn(5000000)) {
throw new Error(translateRaw('GETH_GasLimit'));
}
// Reject gasPrice over 1000gwei (1000000000000)
const gwei = Wei('1000000000000');
if (gasPrice.gt(gwei)) {
throw new Error(
'Gas price too high. Please contact support if this was not a mistake.'
);
}
}
export async function generateCompleteTransactionFromRawTransaction(
node: INode,
tx: ExtendedRawTransaction,
wallet: IWallet,
token: Token | null | undefined,
skipValidation: boolean,
offline?: boolean
): Promise<CompleteTransaction> {
const { to, data, gasLimit, gasPrice, chainId, nonce } = tx;
// validation
generateTxValidation(to, token, data, gasLimit, gasPrice, skipValidation);
// duplicated from generateTxValidation -- typescript bug
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
throw Error('Gas Limit and Gas Price should be of type bignumber');
}
// computed gas cost (gasprice * gaslimit)
const gasCost: Wei = Wei(gasPrice.mul(gasLimit));
// get amount value (either in ETH or in Token)
const value = getValue(token, tx);
// if not offline, ensure that balance exceeds costs
if (!offline) {
await balanceCheck(node, tx, token, value, gasCost);
}
// Taken from v3's `sanitizeHex`, ensures that the value is a %2 === 0
// prefix'd hex value.
const cleanHex = hex => addHexPrefix(padToEven(stripHexPrefixAndLower(hex)));
const cleanedRawTx = {
nonce: cleanHex(nonce),
gasPrice: cleanHex(gasPrice.toString(16)),
gasLimit: cleanHex(gasLimit.toString(16)),
to: toChecksumAddress(cleanHex(to)),
value: token ? '0x00' : cleanHex(value.toString(16)),
data: data ? cleanHex(data) : '',
chainId: chainId || 1
};
// Sign the transaction
const rawTxJson = JSON.stringify(cleanedRawTx);
const signedTx = await wallet.signRawTransaction(cleanedRawTx);
return {
...cleanedRawTx,
rawTx: rawTxJson,
signedTx
};
}
export async function formatTxInput(
wallet: IWallet,
{ token, unit, value, to, data }: TransactionInput
): Promise<TransactionWithoutGas> {
if (unit === 'ether') {
return {
to,
from: await wallet.getAddressString(),
value: toHexWei(value), //turn users ether to wei
data
};
} else {
if (!token) {
throw new Error('No matching token');
}
const bigAmount = TokenValue(value);
const ERC20Data = ERC20.transfer(to, bigAmount);
return {
to: token.address,
from: await wallet.getAddressString(),
value: '0x0',
data: ERC20Data
};
}
}
export async function generateCompleteTransaction(
wallet: IWallet,
nodeLib: RPCNode,
gasPrice: Wei,
gasLimit: Wei,
chainId: number,
transactionInput: TransactionInput,
skipValidation: boolean,
nonce?: number | null,
offline?: boolean
): Promise<CompleteTransaction> {
const { token } = transactionInput;
const { from, to, value, data } = await formatTxInput(
wallet,
transactionInput
);
const transaction: ExtendedRawTransaction = {
nonce: nonce ? `0x${nonce}` : await nodeLib.getTransactionCount(from),
from,
to,
gasLimit,
value,
data,
chainId,
gasPrice
};
return await generateCompleteTransactionFromRawTransaction(
nodeLib,
transaction,
wallet,
token,
skipValidation,
offline
);
}
// TODO determine best place for helper function
export function getBalanceMinusGasCosts(
gasLimit: Wei,
gasPrice: Wei,
balance: Wei
): Wei {
const weiGasCosts = gasPrice.mul(gasLimit);
const weiBalanceMinusGasCosts = balance.sub(weiGasCosts);
return Wei(weiBalanceMinusGasCosts);
}
export function decodeTransaction(transaction: EthTx, token: Token | false) {
const { to, value, data, gasPrice, nonce, from } = getTransactionFields(
transaction
);
let fixedValue: TokenValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = tokenData.value;
toAddress = tokenData.to;
} else {
fixedValue = Wei(value);
toAddress = to;
}
return {
value: fixedValue,
gasPrice: Wei(gasPrice),
data,
toAddress,
nonce,
from
};
}

View File

@ -1,7 +1,10 @@
import { toChecksumAddress } from 'ethereumjs-util';
import { toChecksumAddress, isValidPrivate } from 'ethereumjs-util';
import { RawTransaction } from 'libs/transaction';
import { stripHexPrefix } from 'libs/values';
import WalletAddressValidator from 'wallet-address-validator';
import { normalise } from './ens';
import { Validator } from 'jsonschema';
import { JsonRpcResponse } from './nodes/rpc/types';
export function isValidETHAddress(address: string): boolean {
if (!address) {
@ -86,9 +89,15 @@ function validateEtherAddress(address: string): boolean {
export function isValidPrivKey(privkey: string | Buffer): boolean {
if (typeof privkey === 'string') {
return privkey.length === 64;
const strippedKey = stripHexPrefix(privkey);
const initialCheck = strippedKey.length === 64;
if (initialCheck) {
const keyBuffer = Buffer.from(strippedKey, 'hex');
return isValidPrivate(keyBuffer);
}
return false;
} else if (privkey instanceof Buffer) {
return privkey.length === 32;
return privkey.length === 32 && isValidPrivate(privkey);
} else {
return false;
}
@ -177,3 +186,79 @@ export const isValidByteCode = (byteCode: string) =>
export const isValidAbiJson = (abiJson: string) =>
abiJson && abiJson.startsWith('[') && abiJson.endsWith(']');
// JSONSchema Validations for Rpc responses
const v = new Validator();
export const schema = {
RpcNode: {
type: 'object',
additionalProperties: false,
properties: {
jsonrpc: { type: 'string' },
id: { oneOf: [{ type: 'string' }, { type: 'integer' }] },
result: { oneOf: [{ type: 'string' }, { type: 'array' }] },
status: { type: 'string' },
message: { type: 'string', maxLength: 2 }
}
}
};
function isValidResult(response: JsonRpcResponse, schemaFormat): boolean {
return v.validate(response, schemaFormat).valid;
}
function formatErrors(response: JsonRpcResponse, apiType: string) {
if (response.error) {
return `${response.error.message} ${response.error.data}`;
}
return `Invalid ${apiType} Error`;
}
const isValidEthCall = (response: JsonRpcResponse, schemaType) => (
apiName,
cb?
) => {
if (!isValidResult(response, schemaType)) {
if (cb) {
return cb(response);
}
throw new Error(formatErrors(response, apiName));
}
return response;
};
export const isValidGetBalance = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Get Balance');
export const isValidEstimateGas = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Estimate Gas');
export const isValidCallRequest = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Call Request');
export const isValidTokenBalance = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Token Balance', () => ({
result: 'Failed'
}));
export const isValidTransactionCount = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Transaction Count');
export const isValidCurrentBlock = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Current Block');
export const isValidRawTxApi = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Raw Tx');
export const isValidSendTransaction = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Send Transaction');
export const isValidSignMessage = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Sign Message');
export const isValidGetAccounts = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Get Accounts');
export const isValidGetNetVersion = (response: JsonRpcResponse) =>
isValidEthCall(response, schema.RpcNode)('Net Version');

View File

@ -1,7 +1,18 @@
import { RawTransaction } from 'libs/transaction';
export interface IWallet {
interface IBaseWallet {
isReadOnly?: boolean;
getAddressString(): Promise<string> | string;
}
export interface IReadOnlyWallet extends IBaseWallet {
isReadOnly: true;
}
export interface IFullWallet extends IBaseWallet {
isReadOnly?: false;
signRawTransaction(tx: RawTransaction): Promise<string> | string;
signMessage(msg: string): Promise<string> | string;
}
export type IWallet = IReadOnlyWallet | IFullWallet;

View File

@ -3,10 +3,10 @@ import LedgerEth from 'vendor/ledger-eth';
import EthTx from 'ethereumjs-tx';
import { addHexPrefix, rlp } from 'ethereumjs-util';
import { DeterministicWallet } from './deterministic';
import { IWallet } from '../IWallet';
import { IFullWallet } from '../IWallet';
import { RawTransaction } from 'libs/transaction';
export class LedgerWallet extends DeterministicWallet implements IWallet {
export class LedgerWallet extends DeterministicWallet implements IFullWallet {
private ledger: any;
private ethApp: any;

View File

@ -5,9 +5,9 @@ import { RawTransaction } from 'libs/transaction';
import { stripHexPrefixAndLower } from 'libs/values';
import TrezorConnect from 'vendor/trezor-connect';
import { DeterministicWallet } from './deterministic';
import { IWallet } from '../IWallet';
import { IFullWallet } from '../IWallet';
export class TrezorWallet extends DeterministicWallet implements IWallet {
export class TrezorWallet extends DeterministicWallet implements IFullWallet {
public signRawTransaction(tx: RawTransaction): Promise<string> {
return new Promise((resolve, reject) => {
(TrezorConnect as any).ethereumSignTx(
@ -51,12 +51,12 @@ export class TrezorWallet extends DeterministicWallet implements IWallet {
public signMessage = (message: string): Promise<string> => {
return new Promise((resolve, reject) => {
(TrezorConnect as any).ethereumSignMessage(
this.getPath(),
message,
this.getPath(),
message,
response => {
if (response.success) {
resolve(addHexPrefix(response.signature))
} else{
} else{
console.error(response.error)
reject(response.error)
}

View File

@ -1,4 +1,4 @@
export { IWallet } from './IWallet';
export { IWallet, IReadOnlyWallet, IFullWallet } from './IWallet';
export { Balance } from './balance';
export * from './deterministic';
export * from './non-deterministic';

View File

@ -0,0 +1,14 @@
import { IReadOnlyWallet } from '../IWallet';
export default class AddressOnlyWallet implements IReadOnlyWallet {
public address = '';
public readonly isReadOnly = true;
constructor(address: string) {
this.address = address;
}
public getAddressString() {
return this.address;
}
}

View File

@ -23,7 +23,7 @@ interface ISignWrapper {
unlock();
}
type WrappedWallet = IFullWallet & ISignWrapper;
export type WrappedWallet = IFullWallet & ISignWrapper;
export const signWrapper = (walletToWrap: IFullWallet): WrappedWallet =>
Object.assign(walletToWrap, {

View File

@ -3,6 +3,7 @@ import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
import { signWrapper } from './helpers';
import { decryptPrivKey } from 'libs/decrypt';
import Web3Wallet from './web3';
import AddressOnlyWallet from './address';
const EncryptedPrivateKeyWallet = (
encryptedPrivateKey: string,
@ -26,5 +27,6 @@ export {
MewV1Wallet,
PrivKeyWallet,
UtcWallet,
Web3Wallet
Web3Wallet,
AddressOnlyWallet
};

View File

@ -1,15 +1,18 @@
import { IWallet } from '../IWallet';
import { IFullWallet } from '../IWallet';
import { ExtendedRawTransaction } from 'libs/transaction';
import { networkIdToName } from 'libs/values';
import { networkIdToName, sanitizeHex } from 'libs/values';
import { bufferToHex } from 'ethereumjs-util';
import { configuredStore } from 'store';
import { getNodeLib } from 'selectors/config';
import Web3Node, { isWeb3Node } from 'libs/nodes/web3';
import { INode } from 'libs/nodes/INode';
import BN from 'bn.js';
export default class Web3Wallet implements IWallet {
private web3: any;
export default class Web3Wallet implements IFullWallet {
private address: string;
private network: string;
constructor(web3: any, address: string, network: string) {
this.web3 = web3;
constructor(address: string, network: string) {
this.address = address;
this.network = network;
}
@ -24,69 +27,53 @@ export default class Web3Wallet implements IWallet {
);
}
public signMessage(msg: string): Promise<string> {
return new Promise((resolve, reject) => {
const msgHex = bufferToHex(Buffer.from(msg));
const options = {
method: 'personal_sign',
params: [msgHex, this.address],
signingAddr: this.address
};
public async signMessage(msg: string): Promise<string> {
const msgHex = bufferToHex(Buffer.from(msg));
const state = configuredStore.getState();
const nodeLib: Web3Node | INode = getNodeLib(state);
this.web3.currentProvider.sendAsync(options, (err, data) => {
if (err) {
return reject(err);
}
if (!isWeb3Node(nodeLib)) {
throw new Error('Web3 wallets can only be used with a Web3 node.');
}
if (data.error) {
return reject(data.error);
}
resolve(data.result);
});
});
return nodeLib.signMessage(msgHex, this.address);
}
public sendTransaction(transaction: ExtendedRawTransaction): Promise<string> {
return new Promise((resolve, reject) => {
const { from, to, value, gasLimit, gasPrice, data, nonce } = transaction;
public async sendTransaction(
transaction: ExtendedRawTransaction
): Promise<string> {
const state = configuredStore.getState();
const nodeLib: Web3Node | INode = getNodeLib(state);
const { from, to, value, gasLimit, gasPrice, data, nonce } = transaction;
const web3Tx = {
from,
to,
value,
gas:
gasLimit instanceof BN ? sanitizeHex(gasLimit.toString(16)) : gasLimit,
gasPrice:
gasPrice instanceof BN ? sanitizeHex(gasPrice.toString(16)) : gasPrice,
data,
nonce
};
const web3Tx = {
from,
to,
value,
gas: gasLimit,
gasPrice,
data,
nonce
};
if (!isWeb3Node(nodeLib)) {
throw new Error('Web3 wallets can only be used with a Web3 node.');
}
await this.networkCheck(nodeLib);
// perform sanity check to ensure network hasn't changed
this.web3.version.getNetwork((err1, networkId) => {
const networkName = networkIdToName(networkId);
return nodeLib.sendTransaction(web3Tx);
}
if (err1) {
return reject(err1);
}
if (this.network !== networkName) {
return reject(
new Error(
`Expected MetaMask / Mist network to be ${
this.network
}, but got ${networkName}. ` +
`Please change the network or restart MyEtherWallet.`
)
);
}
// execute transaction
this.web3.eth.sendTransaction(web3Tx, (err2, txHash) => {
if (err2) {
return reject(err2);
}
resolve(txHash);
});
});
});
private async networkCheck(lib: Web3Node) {
const netId = await lib.getNetVersion();
const netName = networkIdToName(netId);
if (this.network !== netName) {
throw new Error(
`Expected MetaMask / Mist network to be ${this.network}, but got ${
netName
}. Please change the network or restart MyEtherWallet.`
);
}
}
}

View File

@ -4,27 +4,35 @@ import {
ChangeNodeAction,
AddCustomNodeAction,
RemoveCustomNodeAction,
AddCustomNetworkAction,
RemoveCustomNetworkAction,
SetLatestBlockAction,
ConfigAction,
ConfigAction
} from 'actions/config';
import { TypeKeys } from 'actions/config/constants';
import {
NODES,
NETWORKS,
NodeConfig,
CustomNodeConfig,
NetworkConfig,
CustomNetworkConfig
} from '../config/data';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
export interface State {
// FIXME
languageSelection: string;
nodeSelection: string;
node: NodeConfig;
network: NetworkConfig;
isChangingNode: boolean;
gasPriceGwei: number;
offline: boolean;
forceOffline: boolean;
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
latestBlock: string;
}
@ -33,12 +41,14 @@ export const INITIAL_STATE: State = {
languageSelection: 'en',
nodeSelection: defaultNode,
node: NODES[defaultNode],
network: NETWORKS[NODES[defaultNode].network],
isChangingNode: false,
gasPriceGwei: 21,
offline: false,
forceOffline: false,
customNodes: [],
latestBlock: "???",
customNetworks: [],
latestBlock: '???'
};
function changeLanguage(state: State, action: ChangeLanguageAction): State {
@ -53,14 +63,14 @@ function changeNode(state: State, action: ChangeNodeAction): State {
...state,
nodeSelection: action.payload.nodeSelection,
node: action.payload.node,
isChangingNode: false,
isChangingNode: false
};
}
function changeNodeIntent(state: State): State {
return {
...state,
isChangingNode: true,
isChangingNode: true
};
}
@ -86,12 +96,13 @@ function forceOffline(state: State): State {
}
function addCustomNode(state: State, action: AddCustomNodeAction): State {
const newId = makeCustomNodeId(action.payload);
return {
...state,
customNodes: [
...state.customNodes,
action.payload,
],
...state.customNodes.filter(node => makeCustomNodeId(node) !== newId),
action.payload
]
};
}
@ -99,16 +110,39 @@ function removeCustomNode(state: State, action: RemoveCustomNodeAction): State {
const id = makeCustomNodeId(action.payload);
return {
...state,
customNodes: state.customNodes.filter((cn) => cn !== action.payload),
nodeSelection: id === state.nodeSelection ?
defaultNode : state.nodeSelection,
customNodes: state.customNodes.filter(cn => cn !== action.payload),
nodeSelection:
id === state.nodeSelection ? defaultNode : state.nodeSelection
};
}
function addCustomNetwork(state: State, action: AddCustomNetworkAction): State {
const newId = makeCustomNetworkId(action.payload);
return {
...state,
customNetworks: [
...state.customNetworks.filter(
node => makeCustomNetworkId(node) !== newId
),
action.payload
]
};
}
function removeCustomNetwork(
state: State,
action: RemoveCustomNetworkAction
): State {
return {
...state,
customNetworks: state.customNetworks.filter(cn => cn !== action.payload)
};
}
function setLatestBlock(state: State, action: SetLatestBlockAction): State {
return {
...state,
latestBlock: action.payload,
latestBlock: action.payload
};
}
@ -133,6 +167,10 @@ export function config(
return addCustomNode(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
return removeCustomNode(state, action);
case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK:
return addCustomNetwork(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK:
return removeCustomNetwork(state, action);
case TypeKeys.CONFIG_SET_LATEST_BLOCK:
return setLatestBlock(state, action);
default:

View File

@ -1,6 +1,5 @@
import { routerReducer } from 'react-router-redux';
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { config, State as ConfigState } from './config';
import { customTokens, State as CustomTokensState } from './customTokens';
import {
@ -39,6 +38,5 @@ export default combineReducers({
customTokens,
rates,
deterministicWallets,
form: formReducer,
routing: routerReducer
});

View File

@ -15,7 +15,10 @@ export interface State {
// in ETH
balance: Balance | { wei: null };
tokens: {
[key: string]: TokenValue;
[key: string]: {
balance: TokenValue;
error: string | null;
};
};
transactions: BroadcastTransactionStatus[];
}

View File

@ -16,7 +16,15 @@ import {
getCustomNodeConfigFromId,
makeNodeConfigFromCustomConfig
} from 'utils/node';
import { getNode, getNodeConfig, getCustomNodeConfigs } from 'selectors/config';
import { makeCustomNetworkId } from 'utils/network';
import {
getNode,
getNodeConfig,
getCustomNodeConfigs,
getCustomNetworkConfigs,
getOffline,
getForceOffline
} from 'selectors/config';
import { AppState } from 'reducers';
import { TypeKeys } from 'actions/config/constants';
import {
@ -24,7 +32,9 @@ import {
changeNode,
changeNodeIntent,
setLatestBlock,
AddCustomNodeAction
removeCustomNetwork,
AddCustomNodeAction,
ChangeNodeIntentAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import translate from 'translations';
@ -38,35 +48,88 @@ import {
export const getConfig = (state: AppState): ConfigState => state.config;
let hasCheckedOnline = false;
export function* pollOfflineStatus(): SagaIterator {
while (true) {
const offline = !navigator.onLine;
const config = yield select(getConfig);
const offlineState = config.offline;
if (offline !== offlineState) {
yield put(toggleOfflineConfig());
const node = yield select(getNodeConfig);
const isOffline = yield select(getOffline);
const isForcedOffline = yield select(getForceOffline);
// If they're forcing themselves offline, exit the loop. It will be
// kicked off again if they toggle it in handleTogglePollOfflineStatus.
if (isForcedOffline) {
return;
}
// If our offline state disagrees with the browser, run a check
// Don't check if the user is in another tab or window
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
if (shouldPing && !document.hidden) {
hasCheckedOnline = true;
const { pingSucceeded } = yield race({
pingSucceeded: call(node.lib.ping.bind(node.lib)),
timeout: call(delay, 5000)
});
if (pingSucceeded && isOffline) {
// If we were able to ping but redux says we're offline, mark online
yield put(
showNotification(
'success',
'Your connection to the network has been restored!',
3000
)
);
yield put(toggleOfflineConfig());
} else if (!pingSucceeded && !isOffline) {
// If we were unable to ping but redux says we're online, mark offline
yield put(
showNotification(
'danger',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
Infinity
)
);
yield put(toggleOfflineConfig());
} else {
// If neither case was true, try again in 5s
yield call(delay, 5000);
}
} else {
yield call(delay, 1000);
}
yield call(delay, 250);
}
}
// Fork our recurring API call, watch for the need to cancel.
function* handlePollOfflineStatus(): SagaIterator {
export function* handlePollOfflineStatus(): SagaIterator {
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
yield cancel(pollOfflineStatusTask);
}
export function* handleTogglePollOfflineStatus(): SagaIterator {
const isForcedOffline = yield select(getForceOffline);
if (isForcedOffline) {
yield fork(handlePollOfflineStatus);
} else {
yield call(handlePollOfflineStatus);
}
}
// @HACK For now we reload the app when doing a language swap to force non-connected
// data to reload. Also the use of timeout to avoid using additional actions for now.
function* reload(): SagaIterator {
export function* reload(): SagaIterator {
setTimeout(() => location.reload(), 250);
}
function* handleNodeChangeIntent(action): SagaIterator {
export function* handleNodeChangeIntent(
action: ChangeNodeIntentAction
): SagaIterator {
const currentNode = yield select(getNode);
const currentConfig = yield select(getNodeConfig);
const currentWallet = yield select(getWalletInst);
const currentNetwork = currentConfig.network;
let actionConfig = NODES[action.payload];
@ -115,6 +178,8 @@ function* handleNodeChangeIntent(action): SagaIterator {
yield put(setLatestBlock(latestBlock));
yield put(changeNode(action.payload, actionConfig));
const currentWallet = yield select(getWalletInst);
// if there's no wallet, do not reload as there's no component state to resync
if (currentWallet && currentNetwork !== actionConfig.network) {
yield call(reload);
@ -126,8 +191,24 @@ export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
yield put(changeNodeIntent(nodeId));
}
// If there are any orphaned custom networks, purge them
export function* cleanCustomNetworks(): SagaIterator {
const customNodes = yield select(getCustomNodeConfigs);
const customNetworks = yield select(getCustomNetworkConfigs);
const networksInUse = customNodes.reduce((prev, conf) => {
prev[conf.network] = true;
return prev;
}, {});
for (const net of customNetworks) {
if (!networksInUse[makeCustomNetworkId(net)]) {
yield put(removeCustomNetwork(net));
}
}
}
// unset web3 as the selected node if a non-web3 wallet has been selected
function* unsetWeb3Node(action): SagaIterator {
export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator {
const node = yield select(getNode);
const nodeConfig = yield select(getNodeConfig);
const newWallet = action.payload;
@ -138,7 +219,24 @@ function* unsetWeb3Node(action): SagaIterator {
}
// switch back to a node with the same network as MetaMask/Mist
const equivalentNode = Object.keys(NODES)
yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig)));
}
export function* unsetWeb3Node(): SagaIterator {
const node = yield select(getNode);
if (node !== 'web3') {
return;
}
const nodeConfig = yield select(getNodeConfig);
const newNode = equivalentNodeOrDefault(nodeConfig);
yield put(changeNodeIntent(newNode));
}
export const equivalentNodeOrDefault = nodeConfig => {
const node = Object.keys(NODES)
.filter(key => key !== 'web3')
.reduce((found, key) => {
const config = NODES[key];
@ -152,21 +250,20 @@ function* unsetWeb3Node(action): SagaIterator {
}, '');
// if no equivalent node was found, use the app default
const newNode = equivalentNode.length
? equivalentNode
: configInitialState.nodeSelection;
yield put(changeNodeIntent(newNode));
}
return node.length ? node : configInitialState.nodeSelection;
};
export default function* configSaga(): SagaIterator {
yield takeLatest(
TypeKeys.CONFIG_POLL_OFFLINE_STATUS,
handlePollOfflineStatus
);
yield takeEvery(TypeKeys.CONFIG_FORCE_OFFLINE, handleTogglePollOfflineStatus);
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3Node);
yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3Node);
yield takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, cleanCustomNetworks);
yield takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node);
yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent);
yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent);
}

View File

@ -25,7 +25,7 @@ import { getTokens } from 'selectors/wallet';
import translate from 'translations';
import { TokenValue } from 'libs/units';
function* getDeterministicWallets(
export function* getDeterministicWallets(
action: GetDeterministicWalletsAction
): SagaIterator {
const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;
@ -64,7 +64,7 @@ function* getDeterministicWallets(
}
// Grab each wallet's main network token, and update it with it
function* updateWalletValues(): SagaIterator {
export function* updateWalletValues(): SagaIterator {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
@ -87,7 +87,7 @@ function* updateWalletValues(): SagaIterator {
}
// Grab the current desired token, and update the wallet with it
function* updateWalletTokenValues(): SagaIterator {
export function* updateWalletTokenValues(): SagaIterator {
const desiredToken: string = yield select(getDesiredToken);
if (!desiredToken) {
return;

View File

@ -1,11 +1,16 @@
import { closeNotification, ShowNotificationAction } from 'actions/notifications';
import {
closeNotification,
ShowNotificationAction
} from 'actions/notifications';
import { delay, SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
function* handleNotification(action: ShowNotificationAction): SagaIterator {
export function* handleNotification(
action: ShowNotificationAction
): SagaIterator {
const { duration } = action.payload;
// show forever
if (duration === 0 || duration === 'infinity') {
if (duration === 0 || duration === Infinity) {
return;
}

View File

@ -31,7 +31,7 @@ import {
export const getSwap = (state: AppState): SwapState => state.swap;
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
const BITY_TIMEOUT_MESSAGE = `
export const BITY_TIMEOUT_MESSAGE = `
Time has run out.
If you have already sent, please wait 1 hour.
If your order has not be processed after 1 hour,
@ -81,7 +81,7 @@ export function* pollBityOrderStatusSaga(): SagaIterator {
}
}
function* postBityOrderCreate(
export function* postBityOrderCreate(
action: BityOrderCreateRequestedSwapAction
): SagaIterator {
const payload = action.payload;
@ -146,7 +146,7 @@ export function* bityTimeRemaining(): SagaIterator {
if (!hasShownNotification) {
hasShownNotification = true;
yield put(
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
);
}
break;
@ -156,7 +156,7 @@ export function* bityTimeRemaining(): SagaIterator {
if (!hasShownNotification) {
hasShownNotification = true;
yield put(
showNotification('danger', BITY_TIMEOUT_MESSAGE, 'infinity')
showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity)
);
}
break;
@ -164,7 +164,7 @@ export function* bityTimeRemaining(): SagaIterator {
if (!hasShownNotification) {
hasShownNotification = true;
yield put(
showNotification('warning', BITY_TIMEOUT_MESSAGE, 'infinity')
showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity)
);
}
break;

View File

@ -20,7 +20,7 @@ export function* loadBityRates(): SagaIterator {
}
// Fork our recurring API call, watch for the need to cancel.
function* handleBityRates(): SagaIterator {
export function* handleBityRates(): SagaIterator {
const loadBityRatesTask = yield fork(loadBityRates);
yield take(TypeKeys.SWAP_STOP_LOAD_BITY_RATES);
yield cancel(loadBityRatesTask);

View File

@ -13,7 +13,8 @@ import {
UnlockPrivateKeyAction
} from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent } from 'actions/config';
import { changeNodeIntent, web3UnsetNode } from 'actions/config';
import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants';
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
import { INode } from 'libs/nodes/INode';
import {
@ -29,17 +30,18 @@ import { SagaIterator } from 'redux-saga';
import {
apply,
call,
cps,
fork,
put,
select,
takeEvery
takeEvery,
take
} from 'redux-saga/effects';
import { getNetworkConfig, getNodeLib } from 'selectors/config';
import { getTokens, getWalletInst } from 'selectors/wallet';
import translate from 'translations';
import Web3Node, { isWeb3Node } from 'libs/nodes/web3';
function* updateAccountBalance(): SagaIterator {
export function* updateAccountBalance(): SagaIterator {
try {
yield put(setBalancePending());
const wallet: null | IWallet = yield select(getWalletInst);
@ -56,7 +58,7 @@ function* updateAccountBalance(): SagaIterator {
}
}
function* updateTokenBalances(): SagaIterator {
export function* updateTokenBalances(): SagaIterator {
try {
const node: INode = yield select(getNodeLib);
const wallet: null | IWallet = yield select(getWalletInst);
@ -87,7 +89,7 @@ function* updateTokenBalances(): SagaIterator {
}
}
function* updateBalances(): SagaIterator {
export function* updateBalances(): SagaIterator {
yield fork(updateAccountBalance);
yield fork(updateTokenBalances);
}
@ -122,7 +124,7 @@ export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator {
yield put(setWallet(wallet));
}
function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
export function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
let wallet;
const { phrase, pass, path, address } = action.payload;
@ -139,38 +141,38 @@ function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
// inspired by v3:
// https://github.com/kvhnuke/etherwallet/blob/417115b0ab4dd2033d9108a1a5c00652d38db68d/app/scripts/controllers/decryptWalletCtrl.js#L311
function* unlockWeb3(): SagaIterator {
const failMsg1 = 'Could not connect to MetaMask / Mist.';
const failMsg2 = 'No accounts found in MetaMask / Mist.';
const { web3 } = window as any;
if (!web3 || !web3.eth) {
yield put(showNotification('danger', translate(failMsg1)));
return;
}
export function* unlockWeb3(): SagaIterator {
try {
yield call(initWeb3Node);
yield put(changeNodeIntent('web3'));
yield take(
action =>
action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE &&
action.payload.nodeSelection === 'web3'
);
const network = NODES.web3.network;
const accounts = yield cps(web3.eth.getAccounts);
const nodeLib: INode | Web3Node = yield select(getNodeLib);
if (!accounts.length) {
yield put(showNotification('danger', translate(failMsg2)));
return;
if (!isWeb3Node(nodeLib)) {
throw new Error('Cannot use Web3 wallet without a Web3 node.');
}
const accounts = yield apply(nodeLib, nodeLib.getAccounts);
const address = accounts[0];
yield put(changeNodeIntent('web3'));
yield put(setWallet(new Web3Wallet(web3, address, network)));
if (!address) {
throw new Error('No accounts found in MetaMask / Mist.');
}
yield put(setWallet(new Web3Wallet(address, network)));
} catch (err) {
console.error(err);
// unset web3 node so node dropdown isn't disabled
yield put(web3UnsetNode());
yield put(showNotification('danger', translate(err.message)));
}
}
function* broadcastTx(action: BroadcastTxRequestedAction): SagaIterator {
export function* broadcastTx(action: BroadcastTxRequestedAction): SagaIterator {
const signedTx = action.payload.signedTx;
try {
const node: INode = yield select(getNodeLib);

1
common/sass/fonts.scss Normal file
View File

@ -0,0 +1 @@
@import './fonts/social-media'

View File

@ -0,0 +1,54 @@
@font-face {
font-family: 'social-media';
src: url('../assets/fonts/social-media.eot');
src: url('../assets/fonts/social-media.eot') format('embedded-opentype'),
url('../assets/fonts/social-media.woff2') format('woff2'),
url('../assets/fonts/social-media.woff') format('woff'),
url('../assets/fonts/social-media.ttf') format('truetype'),
url('../assets/fonts/social-media.svg') format('svg');
font-weight: normal;
font-style: normal;
}
.sm-icon {
display: inline-block;
font: normal normal normal 32px/1 'social-media';
text-transform: none;
/* Better Font Rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
&.sm-16px {
font-size: 16px;
}
&.sm-24px {
font-size: 24px;
}
&.sm-32px {
font-size: 32px;
}
&.sm-48px {
font-size: 48px;
}
// Refer to docs for updating icon-fonts
&.sm-logo-facebook:before {
content: '\ea02';
}
&.sm-logo-reddit:before {
content: '\ea03';
}
&.sm-logo-github:before {
content: '\ea04';
}
&.sm-logo-twitter:before {
content: '\ea05';
}
&.sm-logo-linkedin:before {
content: '\ea06';
}
&.sm-logo-slack:before {
content: '\ea07';
}
&.sm-logo-medium:before {
content: '\ea08';
}
}

View File

@ -27,3 +27,4 @@
@import "./styles/overrides";
@import "./styles/scaffolding";
@import "./styles/tab";
@import "./fonts";

View File

@ -3,6 +3,19 @@
@import "common/sass/mixins";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/alerts";
.alert {
margin-bottom: 1rem;
a {
color: #FFF;
&:hover {
color: #FFF;
opacity: 0.8;
}
}
}
// Alert icons
.alert:after {
content: '';
@ -23,15 +36,6 @@
@media screen and (max-width: $screen-xs) {
left: 1%;
}
a {
color: #FFF;
&:hover {
color: #FFF;
opacity: 0.8;
}
}
}
.alert,

View File

@ -2,9 +2,14 @@
@import 'common/sass/variables';
label {
margin-top: $space-sm;
margin-bottom: $space-xs;
font-size: $font-size-bump-more;
&.is-required:after {
content: '*';
padding-left: 2px;
color: $brand-warning;
}
}
label + .form-control,
@ -39,7 +44,7 @@ input[readonly] {
.form-group {
display: block;
margin-top: $form-group-margin-bottom;
margin-top: $form-group-margin-bottom * 2;
margin-bottom: $form-group-margin-bottom;
}

View File

@ -1,12 +1,13 @@
import {
NetworkConfig,
NetworkContract,
NETWORKS,
NodeConfig,
CustomNodeConfig
CustomNodeConfig,
CustomNetworkConfig
} from 'config/data';
import { INode } from 'libs/nodes/INode';
import { AppState } from 'reducers';
import { getNetworkConfigFromId } from 'utils/network';
export function getNode(state: AppState): string {
return state.config.nodeSelection;
@ -20,12 +21,16 @@ export function getNodeLib(state: AppState): INode {
return getNodeConfig(state).lib;
}
export function getNetworkConfig(state: AppState): NetworkConfig {
return NETWORKS[getNodeConfig(state).network];
export function getNetworkConfig(state: AppState): NetworkConfig | undefined {
return getNetworkConfigFromId(
getNodeConfig(state).network,
getCustomNetworkConfigs(state)
);
}
export function getNetworkContracts(state: AppState): NetworkContract[] | null {
return getNetworkConfig(state).contracts;
const network = getNetworkConfig(state);
return network ? network.contracts : [];
}
export function getGasPriceGwei(state: AppState): number {
@ -39,3 +44,17 @@ export function getLanguageSelection(state: AppState): string {
export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] {
return state.config.customNodes;
}
export function getCustomNetworkConfigs(
state: AppState
): CustomNetworkConfig[] {
return state.config.customNetworks;
}
export function getOffline(state: AppState): boolean {
return state.config.offline;
}
export function getForceOffline(state: AppState): boolean {
return state.config.forceOffline;
}

View File

@ -14,6 +14,7 @@ export interface TokenBalance {
balance: TokenValue;
custom: boolean;
decimal: number;
error: string | null;
}
export type MergedToken = Token & {
@ -21,7 +22,8 @@ export type MergedToken = Token & {
};
export function getTokens(state: AppState): MergedToken[] {
const tokens: Token[] = getNetworkConfig(state).tokens;
const network = getNetworkConfig(state);
const tokens: Token[] = network ? network.tokens : [];
return tokens.concat(
state.customTokens.map((token: Token) => {
const mergedToken = { ...token, custom: true };
@ -38,8 +40,11 @@ export function getTokenBalances(state: AppState): TokenBalance[] {
return tokens.map(t => ({
symbol: t.symbol,
balance: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol].balance
: TokenValue('0'),
error: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol].error
: null,
custom: t.custom,
decimal: t.decimal
}));

View File

@ -109,14 +109,16 @@ const configureStore = () => {
gasPriceGwei: state.config.gasPriceGwei,
nodeSelection: state.config.nodeSelection,
languageSelection: state.config.languageSelection,
customNodes: state.config.customNodes
customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks
},
swap: state.swap,
swap: { ...state.swap, bityRates: {} },
customTokens: state.customTokens
});
}),
1000
);
return store;
};

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