Merge branch 'develop' into develop

This commit is contained in:
Connor Bryan 2018-08-15 15:30:43 -05:00 committed by GitHub
commit 71c76e8afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 3234 additions and 698 deletions

View File

@ -0,0 +1,125 @@
import shapeshift, { SHAPESHIFT_BASE_URL } from './shapeshift';
describe('ShapeShift service', () => {
beforeEach(() => {
(global as any).fetch = jest.fn().mockImplementation(
(url: string) =>
new Promise(resolve => {
const returnValues = {
[`${SHAPESHIFT_BASE_URL}/marketinfo`]: {
status: 200,
json: () => [
{
limit: 1,
maxLimit: 2,
min: 1,
minerFee: 2,
pair: 'BTC_ETH',
rate: '1.0'
},
{
limit: 1,
maxLimit: 2,
min: 1,
minerFee: 2,
pair: 'ETH_BTC',
rate: '1.0'
}
]
},
[`${SHAPESHIFT_BASE_URL}/getcoins`]: {
status: 200,
json: () => ({
BTC: {
name: 'Bitcoin',
symbol: 'BTC',
image: '',
imageSmall: '',
status: 'available',
minerFee: 1
},
ETH: {
name: 'Ethereum',
symbol: 'ETH',
image: '',
imageSmall: '',
status: 'available',
minerFee: 1
},
XMR: {
name: 'Monero',
symbol: 'XMR',
image: '',
imageSmall: '',
status: 'unavailable',
minerFee: 1
}
})
}
};
resolve(returnValues[url]);
})
);
});
it('provides a collection of all available and unavailable coins and tokens', async done => {
const rates = await shapeshift.getAllRates();
expect(rates).toEqual({
BTCETH: {
id: 'BTCETH',
rate: '1.0',
limit: 1,
min: 1,
options: [
{
id: 'BTC',
image: '',
name: 'Bitcoin',
status: 'available'
},
{
id: 'ETH',
image: '',
name: 'Ethereum',
status: 'available'
}
]
},
ETHBTC: {
id: 'ETHBTC',
rate: '1.0',
limit: 1,
min: 1,
options: [
{
id: 'ETH',
image: '',
name: 'Ethereum',
status: 'available'
},
{
id: 'BTC',
image: '',
name: 'Bitcoin',
status: 'available'
}
]
},
__XMR: {
id: '__XMR',
limit: 0,
min: 0,
options: [
{
id: 'XMR',
image: '',
name: 'Monero',
status: 'unavailable'
}
]
}
});
done();
});
});

View File

@ -1,9 +1,12 @@
import flatten from 'lodash/flatten';
import uniqBy from 'lodash/uniqBy';
import { checkHttpStatus, parseJSON } from 'api/utils';
const SHAPESHIFT_API_KEY =
export const SHAPESHIFT_API_KEY =
'8abde0f70ca69d5851702d57b10305705d7333e93263124cc2a2649dab7ff9cf86401fc8de7677e8edcd0e7f1eed5270b1b49be8806937ef95d64839e319e6d9';
const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_TOKEN_WHITELIST = [
'OMG',
@ -26,7 +29,7 @@ export const SHAPESHIFT_TOKEN_WHITELIST = [
'TRST',
'GUP'
];
export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETH', 'ETC', 'BTC'];
export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETH', 'ETC', 'BTC', 'XMR'];
interface IPairData {
limit: number;
@ -66,6 +69,29 @@ interface TokenMap {
};
}
interface ShapeshiftCoinInfo {
image: string;
imageSmall: string;
minerFee: number;
name: string;
status: string;
symbol: string;
}
interface ShapeshiftCoinInfoMap {
[id: string]: ShapeshiftCoinInfo;
}
interface ShapeshiftOption {
id?: string;
status?: string;
image?: string;
}
interface ShapeshiftOptionMap {
[symbol: string]: ShapeshiftOption;
}
class ShapeshiftService {
public whitelist = SHAPESHIFT_WHITELIST;
private url = SHAPESHIFT_BASE_URL;
@ -73,6 +99,8 @@ class ShapeshiftService {
private postHeaders = {
'Content-Type': 'application/json'
};
private supportedCoinsAndTokens: ShapeshiftCoinInfoMap = {};
private fetchedSupportedCoinsAndTokens = false;
public checkStatus(address: string) {
return fetch(`${this.url}/txStat/${address}`)
@ -118,19 +146,76 @@ class ShapeshiftService {
public getAllRates = async () => {
const marketInfo = await this.getMarketInfo();
const pairRates = await this.filterPairs(marketInfo);
const pairRates = this.filterPairs(marketInfo);
const checkAvl = await this.checkAvl(pairRates);
const mappedRates = this.mapMarketInfo(checkAvl);
return mappedRates;
const allRates = this.addUnavailableCoinsAndTokens(mappedRates);
return allRates;
};
public addUnavailableCoinsAndTokens = (availableCoinsAndTokens: TokenMap) => {
if (this.fetchedSupportedCoinsAndTokens) {
/** @desc Create a hash for efficiently checking which tokens are currently available. */
const allOptions = flatten(
Object.values(availableCoinsAndTokens).map(({ options }) => options)
);
const availableOptions: ShapeshiftOptionMap = uniqBy(allOptions, 'id').reduce(
(prev: ShapeshiftOptionMap, next) => {
prev[next.id] = next;
return prev;
},
{}
);
const unavailableCoinsAndTokens = this.whitelist
.map(token => {
/** @desc ShapeShift claims support for the token and it is available. */
const availableCoinOrToken = availableOptions[token];
if (availableCoinOrToken) {
return null;
}
/** @desc ShapeShift claims support for the token, but it is unavailable. */
const supportedCoinOrToken = this.supportedCoinsAndTokens[token];
if (supportedCoinOrToken) {
const { symbol: id, image, name, status } = supportedCoinOrToken;
return {
/** @desc Preface the false id with '__' to differentiate from actual pairs. */
id: `__${id}`,
limit: 0,
min: 0,
options: [{ id, image, name, status }]
};
}
/** @desc We claim support for the coin or token, but ShapeShift doesn't. */
return null;
})
.reduce((prev: ShapeshiftOptionMap, next) => {
if (next) {
prev[next.id] = next;
return prev;
}
return prev;
}, {});
return { ...availableCoinsAndTokens, ...unavailableCoinsAndTokens };
}
return availableCoinsAndTokens;
};
private filterPairs(marketInfo: ShapeshiftMarketInfo[]) {
return marketInfo.filter(obj => {
const { pair } = obj;
const pairArr = pair.split('_');
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1])
? true
: false;
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1]);
});
}
@ -165,7 +250,13 @@ class ShapeshiftService {
private getAvlCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
.then(parseJSON)
.then(supportedCoinsAndTokens => {
this.supportedCoinsAndTokens = supportedCoinsAndTokens;
this.fetchedSupportedCoinsAndTokens = true;
return supportedCoinsAndTokens;
});
}
private getMarketInfo() {

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="217.19"
height="252"
viewBox="0 0 217.19 252"
version="1.1"
id="svg4200"
inkscape:version="0.91 r13725"
sodipodi:docname="safe-t.svg">
<metadata
id="metadata4215">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>trezor</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1381"
id="namedview4213"
showgrid="false"
inkscape:zoom="1.8294574"
inkscape:cx="153.14158"
inkscape:cy="62.034553"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4200" />
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
<title
id="title4202">trezor</title>
<desc
id="desc4204">Created with Sketch.</desc>
<defs
id="defs4206">
<clipPath
id="presentation_clip_path"
clipPathUnits="userSpaceOnUse">
<rect
x="0"
y="0"
width="21000"
height="29700"
id="rect7" />
</clipPath>
</defs>
<g
id="Page-1"
style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1"
transform="translate(0,-6)">
<g
id="trezor"
style="fill:#000000;fill-rule:nonzero">
<g
id="path7" />
</g>
</g>
<g
transform="matrix(0.03542311,0,0,0.03542311,-244.40864,-399.82064)"
class="SlideGroup"
id="g49">
<g
id="g51">
<g
id="id1"
class="Slide"
clip-path="url(#presentation_clip_path)">
<g
class="Page"
id="g54">
<g
class="com.sun.star.drawing.ClosedBezierShape"
id="g56">
<g
id="id3">
<rect
style="fill:none;stroke:none"
class="BoundingBox"
x="6900"
y="13921"
width="6114"
height="4480"
id="rect59" />
<path
style="fill:#000000;stroke:none"
inkscape:connector-curvature="0"
d="m 12880,15423 c 256,0 61,638 61,638 l 0,0 c -9,29 -20,53 -35,79 -4,8 -8,14 -13,22 -402,602 -2104,1772 -2705,2173 -7,5 -14,9 -22,14 -60,35 -120,51 -189,51 -70,0 -130,-16 -190,-51 -8,-5 -15,-9 -22,-14 -600,-401 -2303,-1571 -2705,-2173 -4,-8 -8,-14 -12,-21 -22,-37 -35,-71 -45,-112 -95,-400 -117,-879 -96,-1372 l 48,-735 c 0,0 1690,1634 5925,1501 z"
id="path61" />
</g>
</g>
<g
class="com.sun.star.drawing.ClosedBezierShape"
id="g63">
<g
id="id4">
<rect
style="fill:none;stroke:none"
class="BoundingBox"
x="7033"
y="11287"
width="5998"
height="3839"
id="rect66" />
<path
style="fill:#000000;stroke:none"
inkscape:connector-curvature="0"
d="m 13029,14494 c -77,-1049 -318,-2109 -437,-2592 l 0,0 c -9,-37 -21,-67 -40,-99 -35,-61 -79,-105 -140,-140 -47,-27 -91,-42 -145,-49 -587,-70 -1493,-327 -2283,-327 l -12,0 c -789,0 -1694,256 -2281,327 -55,7 -100,22 -147,49 -61,36 -105,80 -141,141 -19,33 -31,63 -40,99 -83,335 -223,944 -327,1635 l 0,0 c -2,12 -3,23 -3,36 0,45 10,84 33,123 20,34 43,59 76,81 345,234 1322,801 2711,1122 1021,236 2057,283 2745,153 62,-12 113,-31 168,-62 84,-49 145,-109 193,-194 49,-84 71,-167 71,-265 0,-13 0,-24 -1,-38 z m -2621,-1387 0,0 c -21,19 -36,39 -51,64 -25,43 -36,86 -36,136 0,2 0,3 0,5 l 0,687 0,0 c 0,32 -8,58 -23,86 -16,27 -35,46 -62,62 -28,15 -54,23 -86,23 l -235,0 0,0 c -32,0 -58,-8 -85,-23 -27,-16 -47,-35 -62,-62 -16,-28 -23,-54 -23,-86 l 0,-692 0,0 c -1,-50 -13,-92 -38,-136 -16,-28 -34,-49 -58,-70 l 0,0 c -46,-42 -79,-84 -110,-138 -53,-91 -77,-180 -77,-285 0,-105 24,-194 77,-285 52,-91 118,-156 209,-209 91,-53 180,-76 285,-76 105,0 194,23 285,76 91,53 156,118 209,209 52,91 76,180 76,285 0,105 -24,194 -76,285 -33,57 -68,101 -118,144 l -1,0 z"
id="path68" />
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,9 @@
// use dates as vars so they're not stripped out by minification
let letCheck = new Date();
const constCheck = new Date();
const arrowCheck = (() => new Date())();
if (letCheck && constCheck && arrowCheck) {
window.localStorage.setItem('goodBrowser', 'true');
}

View File

@ -0,0 +1,33 @@
var badBrowser = false;
try {
// Local storage
window.localStorage.setItem('test', 'test');
window.localStorage.removeItem('test');
// Flexbox
var elTest = document.createElement('div');
elTest.style.display = 'flex';
if (elTest.style.display !== 'flex') {
badBrowser = true;
}
// const and let check from badBrowserCheckA.js
if (window.localStorage.goodBrowser !== 'true') {
badBrowser = true;
}
window.localStorage.removeItem('goodBrowser');
} catch (err) {
badBrowser = true;
}
if (badBrowser) {
var el = document.getElementsByClassName('BadBrowser')[0];
el.className += ' is-open';
// Dumb check for known mobile OS's. Not important to catch all, just
// displays more appropriate information.
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
el.className += ' is-mobile';
}
}

View File

@ -9,6 +9,7 @@ interface Props {
hasUnitDropdown?: boolean;
hasSendEverything?: boolean;
showAllTokens?: boolean;
showInvalidWithoutValue?: boolean;
customValidator?(rawAmount: string): boolean;
}
@ -16,7 +17,8 @@ export const AmountField: React.SFC<Props> = ({
hasUnitDropdown,
hasSendEverything,
showAllTokens,
customValidator
customValidator,
showInvalidWithoutValue
}) => (
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
@ -30,6 +32,7 @@ export const AmountField: React.SFC<Props> = ({
value={raw}
readOnly={!!readOnly}
onChange={onChange}
showInvalidWithoutValue={showInvalidWithoutValue}
/>
{hasSendEverything && <SendEverything />}
{hasUnitDropdown && <UnitDropDown showAllTokens={showAllTokens} />}

View File

@ -1,53 +0,0 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.AppAlpha {
@include cover-message;
background: color(brand-info);
left: $electron-sidebar-width - 1;
&-content {
h2 {
text-align: center;
}
p {
text-align: justify;
}
&-btn {
display: block;
width: 100%;
max-width: 280px;
margin: 40px auto 0;
border: none;
padding: 0;
transition: $transition;
height: 60px;
line-height: 60px;
font-size: 22px;
background: #fff;
color: #333;
opacity: 0.96;
border-radius: 4px;
&:hover {
opacity: 1;
}
}
}
// Fade out
&.is-fading {
pointer-events: none;
opacity: 0;
background: color(control-bg);
transition: all 500ms ease 400ms;
.AppAlpha-content {
opacity: 0;
transform: translateY(15px);
transition: all 500ms ease;
}
}
}

View File

@ -1,73 +0,0 @@
import React from 'react';
import moment from 'moment';
import { discordURL, APP_ALPHA_EXPIRATION } from 'config';
import { NewTabLink } from 'components/ui';
import './AlphaNotice.scss';
interface State {
isFading: boolean;
isClosed: boolean;
}
let hasAcknowledged = false;
export default class AppAlphaNotice extends React.PureComponent<{}, State> {
public state = {
isFading: false,
isClosed: hasAcknowledged
};
public render() {
if (this.state.isClosed) {
return null;
}
const isFading = this.state.isFading ? 'is-fading' : '';
const expDate = moment(APP_ALPHA_EXPIRATION).format('MMMM Do, YYYY');
return (
<div className={`AppAlpha ${isFading}`}>
<div className="AppAlpha-content">
<h2>Welcome to the MyCrypto Desktop App Alpha</h2>
<p>
Thank you for testing out the new MyCrypto desktop app. This is an early release to be
tested by the community before a full launch. We recommend continuing to use the
production site for large or otherwise important transactions.
</p>
<p>
Because this is for testing purposes only,{' '}
<strong>this build of the app will only be accessible until {expDate}</strong>. Youll
then be required to update the application to continue using it.
</p>
<p>
Feedback and bug reports are greatly appreciated. You can file issues on our{' '}
<NewTabLink href="https://github.com/MyCryptoHQ/MyCrypto/issues">
GitHub repository
</NewTabLink>{' '}
or join our <NewTabLink href={discordURL}>Discord server</NewTabLink> to discuss the
app.
</p>
<p>
<b>
For critical reports & vulnerabilities, please use{' '}
<NewTabLink href="https://hackerone.com/MyCrypto">HackerOne</NewTabLink>.
</b>
</p>
<button className="AppAlpha-content-btn is-continue" onClick={this.doContinue}>
Continue to the Alpha
</button>
</div>
</div>
);
}
private doContinue = () => {
hasAcknowledged = true;
this.setState({ isFading: true });
setTimeout(() => {
this.setState({ isClosed: true });
}, 1000);
};
}

View File

@ -1,44 +0,0 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.AppExpired {
@include cover-message;
background: color(brand-danger);
display: flex;
align-items: center;
&-content {
padding-bottom: 60px;
h2 {
text-align: center;
}
p {
text-align: justify;
}
&-btn {
display: block;
width: 100%;
max-width: 280px;
margin: 40px auto 0;
text-align: center;
border: none;
padding: 0;
transition: $transition;
height: 60px;
line-height: 60px;
font-size: 22px;
background: #fff;
color: #333;
border-radius: 4px;
text-shadow: none;
opacity: 0.95;
&:hover {
opacity: 1;
}
}
}
}

View File

@ -1,26 +0,0 @@
import React from 'react';
import { NewTabLink } from 'components/ui';
import './AppExpired.scss';
const AppExpired: React.SFC<{}> = () => (
<div className="AppExpired">
<div className="AppExpired-content">
<h2>Your Alpha Build Has Expired</h2>
<p>
To ensure the safety of your funds, we are expiring alpha builds one month after release and
requiring users to update. All you have to do is download a new build from our GitHub, and
you can continue to use the app. Sorry for the hassle!
</p>
<NewTabLink
href="https://github.com/MyCryptoHQ/MyCrypto/releases/latest"
className="AppExpired-content-btn"
>
Download a New Build
</NewTabLink>
</div>
</div>
);
export default AppExpired;

View File

@ -1,15 +0,0 @@
import React from 'react';
import { APP_ALPHA_EXPIRATION } from 'config';
import AlphaNotice from './AlphaNotice';
import AppExpired from './AppExpired';
const AppAlphaNotice: React.SFC<{}> = () => {
if (APP_ALPHA_EXPIRATION < Date.now()) {
return <AppExpired />;
} else {
return <AlphaNotice />;
}
};
export default AppAlphaNotice;

View File

@ -13,17 +13,7 @@ const CarouselAnimation = ({ children, ...props }: any) => (
);
// Don't change Coinbase index
const promos = [HardwareWallets, Coinbase, Shapeshift];
const isEuroLocal = () => {
// getTimezoneOffset returns the difference in minutes between UTC and local time.
// the offset is positive if behind UTC (like UTC-4), and negative if above (like UTC+2)
const offset = new Date().getTimezoneOffset();
// -240 to 0 covers UTC+4 to UTC+0, which is all of europe
return -240 <= offset && offset < 0;
};
if (isEuroLocal()) {
promos.push(Simplex);
}
const promos = [HardwareWallets, Coinbase, Shapeshift, Simplex];
interface State {
activePromo: number;
@ -35,7 +25,6 @@ interface StateProps {
class PromosClass extends React.PureComponent<StateProps, State> {
public timer: any = null;
public state = {
activePromo: parseInt(String(Math.random() * promos.length), 10)
};

View File

@ -100,6 +100,7 @@ class ConfirmationModalTemplateClass extends React.Component<Props, State> {
buttons={buttons}
handleClose={onClose}
disableButtons={transactionBroadcasting}
hideButtons={transactionBroadcasting}
isOpen={isOpen}
>
{transactionBroadcasting ? (

View File

@ -252,7 +252,13 @@ class CustomNodeModal extends React.Component<Props, State> {
}
private pollForDefaultNodes() {
return null;
// @ts-ignore
const pollingInterval = 3000;
// console.warning in production to explain to users why we are making a call to localhost
console.warn(
"Don't panic! MyCrypto is going to start a poll for default nodes on port 8545. If you don't like this feature, send us a ping at support@mycrypto.com and we'll walk you through disabling it."
);
this.timer = window.setInterval(async () => {
const results = await exists(
[

View File

@ -44,7 +44,6 @@ class ElectronNav extends React.Component<Props, State> {
>
<div className="ElectronNav-branding">
<div className="ElectronNav-branding-logo" onClick={this.toggleTheme} />
<div className="ElectronNav-branding-beta">Alpha Release</div>
</div>
<ul className="ElectronNav-links">
@ -54,6 +53,7 @@ class ElectronNav extends React.Component<Props, State> {
link={link}
isHomepage={link === navigationLinks[0]}
className="ElectronNavLink"
isNotEnabled={false}
/>
))}
</ul>

View File

@ -3,9 +3,11 @@ import React, { PureComponent } from 'react';
import { navigationLinks } from 'config';
import NavigationLink from 'components/NavigationLink';
import './Navigation.scss';
import { TAB } from './constants';
interface Props {
color?: string | false;
unsupportedTabs?: TAB[];
}
interface State {
@ -30,7 +32,7 @@ export default class Navigation extends PureComponent<Props, State> {
*/
public render() {
const { color } = this.props;
const { color, unsupportedTabs } = this.props;
const borderStyle: BorderStyle = {};
if (color) {
@ -58,6 +60,9 @@ export default class Navigation extends PureComponent<Props, State> {
link={link}
isHomepage={link === navigationLinks[0]}
className="NavigationLink"
isNotEnabled={
unsupportedTabs && unsupportedTabs.map(tab => tab.toString()).includes(link.name)
}
/>
))}
</ul>

View File

@ -0,0 +1,10 @@
export enum TAB {
VIEW = 'NAV_VIEW',
CREATE = 'NAV_GENERATEWALLET',
SWAP = 'NAV_SWAP',
CONTRACTS = 'NAV_CONTRACTS',
ENS = 'NAV_ENS',
SIGN = 'NAV_SIGN',
TXSTATUS = 'NAV_TXSTATUS',
BROADCAST = 'NAV_BROADCAST'
}

View File

@ -122,7 +122,10 @@ class Header extends Component<Props, State> {
</section>
</section>
<Navigation color={!network.isCustom && network.color} />
<Navigation
color={!network.isCustom && network.color}
unsupportedTabs={network.unsupportedTabs}
/>
<CustomNodeModal
isOpen={isAddingCustomNode}

View File

@ -9,13 +9,18 @@ interface Props extends RouteComponentProps<{}> {
link: NavigationLink;
isHomepage: boolean;
className: string;
isNotEnabled?: boolean;
}
class NavigationLinkClass extends React.PureComponent<Props, {}> {
public render() {
const { link, location, isHomepage, className } = this.props;
const { link, location, isHomepage, className, isNotEnabled } = this.props;
let isActive = false;
if (isNotEnabled) {
return null;
}
if (!link.external) {
// isActive if
// 1) Current path is the same as link

View File

@ -12,6 +12,7 @@ import './NonceField.scss';
interface OwnProps {
alwaysDisplay: boolean;
showInvalidBeforeBlur?: boolean;
}
interface StateProps {
@ -27,7 +28,13 @@ type Props = OwnProps & DispatchProps & StateProps;
class NonceField extends React.Component<Props> {
public render() {
const { alwaysDisplay, requestNonce, noncePending, isOffline } = this.props;
const {
alwaysDisplay,
showInvalidBeforeBlur,
requestNonce,
noncePending,
isOffline
} = this.props;
return (
<NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
@ -51,6 +58,7 @@ class NonceField extends React.Component<Props> {
onChange={onChange}
disabled={noncePending}
showInvalidWithoutValue={true}
showInvalidBeforeBlur={showInvalidBeforeBlur}
/>
{noncePending ? (
<div className="Nonce-spinner">

View File

@ -106,7 +106,7 @@ class AdvancedGas extends React.Component<Props, State> {
)}
{nonceField && (
<div className="AdvancedGas-nonce">
<NonceField alwaysDisplay={true} />
<NonceField alwaysDisplay={true} showInvalidBeforeBlur={true} />
</div>
)}
</div>

View File

@ -76,6 +76,16 @@ class SimpleGas extends React.Component<Props> {
min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.min
};
/**
* @desc On retrieval of gas estimates,
* the current gas price may be lower than the lowest recommended price.
* `rc-slider` will force the onChange if the value is too low, so we
* ensure it at least passes the lower boundary.
* When this occurs, the logic in `UNSAFE_componentWillReceiveProps` fires,
* and it cannot happen again from that point forward.
*/
const actualGasPrice = Math.max(this.getGasPriceGwei(gasPrice.value), bounds.min);
return (
<div className="SimpleGas row form-group">
<div className="SimpleGas-title">
@ -103,7 +113,7 @@ class SimpleGas extends React.Component<Props> {
min={bounds.min}
max={bounds.max}
step={bounds.min < 1 ? 0.1 : 1}
value={this.getGasPriceGwei(gasPrice.value)}
value={actualGasPrice}
tipFormatter={this.formatTooltip}
disabled={isGasEstimating}
/>

View File

@ -17,7 +17,7 @@ $speed: 500ms;
@mixin decrypt-title {
text-align: center;
line-height: 1;
margin: 0 0 30px;
margin: $space 0 2rem;
font-weight: normal;
animation: decrypt-enter $speed ease 1;
}
@ -50,7 +50,7 @@ $speed: 500ms;
&-generate {
text-align: center;
font-weight: 300;
margin-top: $space;
margin: $space 0;
}
}

View File

@ -6,6 +6,7 @@ import { Link } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty';
import {
HardwareWalletName,
SecureWalletName,
InsecureWalletName,
MiscWalletName,
@ -23,6 +24,7 @@ import { transactionFieldsActions } from 'features/transaction';
import { notificationsActions } from 'features/notifications';
import LedgerIcon from 'assets/images/wallets/ledger.svg';
import TrezorIcon from 'assets/images/wallets/trezor.svg';
import SafeTIcon from 'assets/images/wallets/safe-t.svg';
import ParitySignerIcon from 'assets/images/wallets/parity-signer.svg';
import { Errorable } from 'components';
import { DisabledWallets } from './disables';
@ -34,6 +36,7 @@ import {
PrivateKeyDecrypt,
PrivateKeyValue,
TrezorDecrypt,
SafeTminiDecrypt,
ViewOnlyDecrypt,
Web3Decrypt,
WalletButton,
@ -96,12 +99,17 @@ export interface InsecureWalletInfo extends BaseWalletInfo {
// tslint:disable-next-line:no-empty-interface
interface MiscWalletInfo extends InsecureWalletInfo {}
type HardwareWallets = { [key in HardwareWalletName]: SecureWalletInfo };
type SecureWallets = { [key in SecureWalletName]: SecureWalletInfo };
type InsecureWallets = { [key in InsecureWalletName]: InsecureWalletInfo };
type MiscWallet = { [key in MiscWalletName]: MiscWalletInfo };
type Wallets = SecureWallets & InsecureWallets & MiscWallet;
type Wallets = HardwareWallets & SecureWallets & InsecureWallets & MiscWallet;
const SECURE_WALLETS = Object.values(SecureWalletName);
const HARDWARE_WALLETS = Object.values(HardwareWalletName);
/** @desc Hardware wallets are secure too, but we want to avoid duplication. */
const SECURE_WALLETS = Object.values(SecureWalletName).filter(
value => !HARDWARE_WALLETS.includes(value)
);
const INSECURE_WALLETS = Object.values(InsecureWalletName);
const MISC_WALLETS = Object.values(MiscWalletName);
@ -141,6 +149,16 @@ const WalletDecrypt = withRouter<Props>(
helpLink:
'https://support.mycrypto.com/accessing-your-wallet/how-to-use-your-trezor-with-mycrypto.html'
},
[SecureWalletName.SAFE_T]: {
lid: 'X_SAFE_T',
icon: SafeTIcon,
description: 'ADD_HARDWAREDESC',
component: SafeTminiDecrypt,
initialParams: {},
unlock: this.props.setWallet,
// TODO - Update with the right id once available
helpLink: 'https://www.archos.com/fr/products/crypto/faq.html'
},
[SecureWalletName.PARITY_SIGNER]: {
lid: 'X_PARITYSIGNER',
icon: ParitySignerIcon,
@ -297,7 +315,7 @@ const WalletDecrypt = withRouter<Props>(
<h2 className="WalletDecrypt-wallets-title">{translate('DECRYPT_ACCESS')}</h2>
<div className="WalletDecrypt-wallets-row">
{SECURE_WALLETS.map((walletType: SecureWalletName) => {
{HARDWARE_WALLETS.map((walletType: SecureWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
@ -316,23 +334,22 @@ const WalletDecrypt = withRouter<Props>(
})}
</div>
<div className="WalletDecrypt-wallets-row">
{INSECURE_WALLETS.map((walletType: InsecureWalletName) => {
{SECURE_WALLETS.map((walletType: SecureWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
key={walletType}
name={translateRaw(wallet.lid)}
example={wallet.example}
description={translateRaw(wallet.description)}
helpLink={wallet.helpLink}
walletType={walletType}
isSecure={false}
isSecure={true}
isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice}
/>
);
})}
{MISC_WALLETS.map((walletType: MiscWalletName) => {
const wallet = this.WALLETS[walletType];
return (
@ -343,6 +360,26 @@ const WalletDecrypt = withRouter<Props>(
helpLink={wallet.helpLink}
walletType={walletType}
isReadOnly={true}
isSecure={true}
isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice}
/>
);
})}
</div>
<div className="WalletDecrypt-wallets-row">
{INSECURE_WALLETS.map((walletType: InsecureWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
key={walletType}
name={translateRaw(wallet.lid)}
example={wallet.example}
helpLink={wallet.helpLink}
walletType={walletType}
isSecure={false}
isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice}

View File

@ -17,6 +17,13 @@ interface Props {
onUnlock(param: any): void;
}
interface SignerAddress {
address: string;
chainId: number;
}
type SignerQrContent = SignerAddress | string;
class ParitySignerDecryptClass extends PureComponent<Props> {
public render() {
return (
@ -36,13 +43,13 @@ class ParitySignerDecryptClass extends PureComponent<Props> {
);
}
private unlockAddress = (address: string) => {
if (!isValidETHAddress(address)) {
private unlockAddress = (content: SignerQrContent) => {
if (typeof content === 'string' || !isValidETHAddress(content.address)) {
this.props.showNotification('danger', 'Not a valid address!');
return;
}
this.props.onUnlock(new ParitySignerWallet(address));
this.props.onUnlock(new ParitySignerWallet(content.address));
};
}

View File

@ -0,0 +1,31 @@
.SafeTminiDecrypt {
text-align: center;
&-help {
margin-top: 10px;
font-size: 13px;
}
&-error {
opacity: 0;
transition: none;
&.is-showing {
opacity: 1;
}
}
&-buy {
margin: 10px 0;
}
&-message {
display: flex;
justify-content: center;
align-items: center;
.Spinner {
margin-right: 16px;
}
}
}

View File

@ -0,0 +1,153 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { SecureWalletName, safeTReferralURL } from 'config';
import translate, { translateRaw } from 'translations';
import { SafeTWallet } from 'libs/wallet';
import { AppState } from 'features/reducers';
import { getSingleDPath, getPaths } from 'features/config';
import { Spinner, NewTabLink } from 'components/ui';
import UnsupportedNetwork from './UnsupportedNetwork';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import './SafeT.scss';
//todo: conflicts with comment in walletDecrypt -> onUnlock method
interface OwnProps {
onUnlock(param: any): void;
}
interface StateProps {
dPath: DPath | undefined;
dPaths: DPath[];
}
// todo: nearly duplicates ledger component props
interface State {
publicKey: string;
chainCode: string;
dPath: DPath;
error: string | null;
isLoading: boolean;
}
type Props = OwnProps & StateProps;
class SafeTminiDecryptClass extends PureComponent<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: this.props.dPath || this.props.dPaths[0],
error: null,
isLoading: false
};
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath && nextProps.dPath) {
this.setState({ dPath: nextProps.dPath });
}
}
public render() {
const { dPath, publicKey, chainCode, error, isLoading } = this.state;
const showErr = error ? 'is-showing' : '';
if (!dPath) {
return <UnsupportedNetwork walletType={translateRaw('X_SAFE_T')} />;
}
// todo: update help link
return (
<div className="SafeTminiDecrypt">
<button
className="SafeTminiDecrypt-decrypt btn btn-primary btn-lg btn-block"
onClick={this.handleNullConnect}
disabled={isLoading}
>
{isLoading ? (
<div className="SafeTminiDecrypt-message">
<Spinner light={true} />
{translate('WALLET_UNLOCKING')}
</div>
) : (
translate('ADD_SAFE_T_SCAN')
)}
</button>
<NewTabLink className="SafeTminiDecrypt-buy btn btn-sm btn-default" href={safeTReferralURL}>
{translate('ORDER_SAFE_T')}
</NewTabLink>
<div className={`SafeTminiDecrypt-error alert alert-danger ${showErr}`}>{error || '-'}</div>
<DeterministicWalletsModal
isOpen={!!publicKey && !!chainCode}
publicKey={publicKey}
chainCode={chainCode}
dPath={dPath}
dPaths={this.props.dPaths}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
/>
</div>
);
}
private handlePathChange = (dPath: DPath) => {
this.setState({ dPath });
this.handleConnect(dPath);
};
private handleConnect = (dPath: DPath): void => {
this.setState({
isLoading: true,
error: null
});
SafeTWallet.getChainCode(dPath.value)
.then(res => {
this.setState({
dPath,
publicKey: res.publicKey,
chainCode: res.chainCode,
isLoading: false
});
})
.catch(err => {
this.setState({
error: err.message,
isLoading: false
});
});
};
private handleCancel = () => {
this.reset();
};
private handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new SafeTWallet(address, this.state.dPath.value, index));
this.reset();
};
private handleNullConnect = (): void => {
this.handleConnect(this.state.dPath);
};
private reset() {
this.setState({
publicKey: '',
chainCode: '',
dPath: this.props.dPath || this.props.dPaths[0]
});
}
}
function mapStateToProps(state: AppState): StateProps {
return {
dPath: getSingleDPath(state, SecureWalletName.SAFE_T),
dPaths: getPaths(state, SecureWalletName.SAFE_T)
};
}
export const SafeTminiDecrypt = connect(mapStateToProps)(SafeTminiDecryptClass);

View File

@ -30,7 +30,7 @@
animation: wallet-button-enter 400ms ease 1;
animation-fill-mode: backwards;
@for $i from 0 to 5 {
@for $i from 0 to 6 {
&:nth-child(#{$i}) {
animation-delay: 100ms + ($i * 60ms);
}

View File

@ -7,6 +7,7 @@ export * from './Mnemonic';
export * from './ParitySigner';
export * from './PrivateKey';
export * from './Trezor';
export * from './SafeT';
export * from './ViewOnly';
export * from './WalletButton';
export * from './Web3';

View File

@ -22,9 +22,10 @@ export const DISABLE_WALLETS: { [key in WalletMode]: DisabledWallets } = {
}
},
[WalletMode.UNABLE_TO_SIGN]: {
wallets: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
wallets: [SecureWalletName.TREZOR, SecureWalletName.SAFE_T, MiscWalletName.VIEW_ONLY],
reasons: {
[SecureWalletName.TREZOR]: 'This wallet cant sign messages',
[SecureWalletName.SAFE_T]: 'This wallet cant sign messages',
[MiscWalletName.VIEW_ONLY]: 'This wallet cant sign messages'
}
}

View File

@ -24,4 +24,3 @@ export { default as ParityQrSigner } from './ParityQrSigner';
export { default as ElectronNav } from './ElectronNav';
export { default as AddressBookTable } from './AddressBookTable';
export { default as Errorable } from './Errorable';
export { default as AppAlphaNotice } from './AppAlphaNotice';

View File

@ -9,7 +9,7 @@ interface Props extends ReactSelectProps {
export default class Dropdown extends React.Component<Props> {
public state = {
selectedOption: { value: '', label: '' },
selectedOption: { value: undefined, label: '' },
hasBlurred: false
};
@ -43,13 +43,13 @@ export default class Dropdown extends React.Component<Props> {
});
}
}}
{...this.props}
className={`${this.props.className} ${this.state.hasBlurred ? 'has-blurred' : ''}`}
value={value}
onChange={obj => {
this.handleChange(obj as any);
onChange();
onChange(obj as any);
}}
{...this.props}
onBlur={e => {
this.setState({ hasBlurred: true });
if (this.props && this.props.onBlur) {

View File

@ -49,7 +49,7 @@ class Input extends React.Component<Props, State> {
} else if (!hasBlurred && !showInvalidBeforeBlur) {
validClass = '';
}
if (!hasValue && showInvalidWithoutValue) {
if ((!isStateless || showInvalidBeforeBlur) && !hasValue && showInvalidWithoutValue) {
validClass = 'invalid';
}

View File

@ -9,7 +9,8 @@ interface Props {
modalStyle?: CSSProperties;
hasButtons?: number;
buttons?: IButton[];
disableButtons?: any;
disableButtons?: boolean;
hideButtons?: boolean;
handleClose(): void;
}
@ -45,7 +46,7 @@ export default class ModalBody extends React.Component<Props> {
};
public render() {
const { title, children, modalStyle, hasButtons, handleClose } = this.props;
const { title, children, modalStyle, hasButtons, hideButtons, handleClose } = this.props;
return (
<div
className="Modal"
@ -68,9 +69,9 @@ export default class ModalBody extends React.Component<Props> {
<div className="Modal-content" ref={div => (this.modalContent = div as HTMLElement)}>
{children}
<div className={`Modal-fade ${!hasButtons ? 'has-no-footer' : ''}`} />
<div className={`Modal-fade ${!hasButtons || hideButtons ? 'has-no-footer' : ''}`} />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
{hasButtons && !hideButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>
);
}

View File

@ -15,6 +15,7 @@ interface Props {
isOpen?: boolean;
title?: React.ReactNode;
disableButtons?: boolean;
hideButtons?: boolean;
children: React.ReactNode;
buttons?: IButton[];
maxWidth?: number;
@ -56,7 +57,16 @@ export default class Modal extends PureComponent<Props, {}> {
}
public render() {
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
const {
isOpen,
title,
children,
buttons,
disableButtons,
hideButtons,
handleClose,
maxWidth
} = this.props;
const hasButtons = buttons && buttons.length;
const modalStyle: ModalStyle = {};
@ -65,7 +75,16 @@ export default class Modal extends PureComponent<Props, {}> {
modalStyle.maxWidth = `${maxWidth}px`;
}
const modalBodyProps = { title, children, modalStyle, hasButtons, buttons, handleClose };
const modalBodyProps = {
title,
children,
modalStyle,
hasButtons,
buttons,
disableButtons,
hideButtons,
handleClose
};
const modal = (
<TransitionGroup>

View File

@ -87,7 +87,7 @@ class SwapDropdown extends PureComponent<Props, State> {
key={opt.name}
option={opt}
isMain={false}
isDisabled={opt.name === disabledOption}
isDisabled={opt.name === disabledOption || opt.status === 'unavailable'}
onChange={this.handleChange}
/>
))}
@ -140,6 +140,23 @@ class SwapDropdown extends PureComponent<Props, State> {
(opt1, opt2) => (opt1.id.toLowerCase() > opt2.id.toLowerCase() ? 1 : -1)
);
// Sort unavailable options last
otherOptions = otherOptions.sort((opt1, opt2) => {
if (opt1.status === 'available' && opt2.status === 'unavailable') {
return -1;
}
if (opt1.status === 'available' && opt2.status === 'available') {
return 0;
}
if (opt1.status === 'unavailable' && opt2.status === 'available') {
return 1;
}
return 0;
});
this.setState({ mainOptions, otherOptions });
}
}

View File

@ -0,0 +1,35 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.Warning {
display: flex;
align-items: center;
border-top: 2px solid $brand-danger;
padding: $space-sm;
font-size: $font-size-base;
line-height: 1.5;
font-weight: 500;
box-shadow: 0 1px 1px 1px rgba(#000, 0.12);
margin-bottom: $space;
&.highlighted {
background-color: lighten($brand-danger, 30%);
}
&-icon {
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
margin-left: $space * 0.4;
margin-right: $space * 0.8;
text-align: center;
font-size: 32px;
color: $brand-danger;
}
&-content {
display: flex;
flex-direction: column;
padding: 0 $space;
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import './Warning.scss';
interface WarningProps {
highlighted?: boolean;
}
const Warning: React.SFC<WarningProps> = ({ highlighted, children }) => {
const className = `Warning ${highlighted ? 'highlighted' : ''}`;
return (
<section className={className}>
<section className="Warning-icon">
<i className="fa fa-exclamation-triangle" />
</section>
<section className="Warning-content">{children}</section>
</section>
);
};
export default Warning;

View File

@ -16,5 +16,6 @@ export { default as TextArea } from './TextArea';
export { default as Address } from './Address';
export { default as CodeBlock } from './CodeBlock';
export { default as Toggle } from './Toggle';
export { default as Warning } from './Warning';
export * from './Expandable';
export * from './InlineSpinner';

View File

@ -1,6 +1,6 @@
import { BTCTxExplorer, ETHTxExplorer } from './data';
export type WhitelistedCoins = 'BTC' | 'REP' | 'ETH';
export type WhitelistedCoins = 'BTC' | 'REP' | 'ETH' | 'XMR';
const serverURL = 'https://bity.myetherapi.com';
const bityURL = 'https://bity.com/api';
const BTCMin = 0.01;
@ -11,7 +11,8 @@ const BTCMax = 3;
// value = percent higher/lower than 0.01 BTC worth
const buffers = {
ETH: 0.1,
REP: 0.2
REP: 0.2,
XMR: 0.3
};
// rate must be BTC[KIND]

View File

@ -0,0 +1,8 @@
[
{
"name": "Bridge",
"address": "0x0000000000000000000000000000000001000006",
"abi":
"[{ \"name\": \"getFederationAddress\", \"type\": \"function\", \"constant\": true, \"inputs\": [], \"outputs\": [{ \"name\": \"\", \"type\": \"string\" }] }]"
}
]

View File

@ -9,17 +9,9 @@ export const languages = require('./languages.json');
export const discordURL = 'https://discord.gg/VSaTXEA';
// Displays in the footer
const VERSION_ELECTRON = packageJson['electron-version'];
const VERSION_WEB = packageJson.version;
export const VERSION = process.env.BUILD_ELECTRON ? VERSION_ELECTRON : VERSION_WEB;
export const VERSION = packageJson.version;
export const N_FACTOR = 8192;
// Bricks the app once this date has been exceeded. Remember to update these 2
// whenever making a new app release.
// It is currently set to: Wednesday, July 25, 2018 12:00:00 AM (GMT)
// TODO: Remove me once app alpha / release candidates are done
export const APP_ALPHA_EXPIRATION = 1532476800000;
// Displays at the top of the site, make message empty string to remove.
// Type can be primary, warning, danger, success, info, or blank for grey.
// Message must be a JSX element if you want to use HTML.
@ -46,7 +38,9 @@ export const etherChainExplorerInst = makeExplorer({
export const donationAddressMap = {
BTC: '32oirLEzZRhi33RCXDF9WHJjEb8RsrSss3',
ETH: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520'
REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
XMR:
'4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CW7F7tDFvS511SNfZV7'
};
export const gasEstimateCacheTime = 60000;
@ -61,6 +55,9 @@ export const MINIMUM_PASSWORD_LENGTH = 12;
export const knowledgeBaseURL = 'https://support.mycrypto.com';
export const ledgerReferralURL = 'https://www.ledgerwallet.com/r/1985?path=/products/';
export const trezorReferralURL = 'https://shop.trezor.io?a=mycrypto.com';
// TODO - Update url
export const safeTReferralURL =
'https://www.archos.com/fr/products/crypto/archos_safetmini/index.html';
export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mycrypto';
// TODO - Update url, this is MEW's
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
@ -75,12 +72,14 @@ export enum SecureWalletName {
WEB3 = 'web3',
LEDGER_NANO_S = 'ledgerNanoS',
TREZOR = 'trezor',
SAFE_T = 'safeTmini',
PARITY_SIGNER = 'paritySigner'
}
export enum HardwareWalletName {
LEDGER_NANO_S = 'ledgerNanoS',
TREZOR = 'trezor'
TREZOR = 'trezor',
SAFE_T = 'safeTmini'
}
export enum InsecureWalletName {

View File

@ -8,6 +8,11 @@ export const ETH_TREZOR: DPath = {
value: "m/44'/60'/0'/0"
};
export const ETH_SAFE_T: DPath = {
label: 'Safe-T (ETH)',
value: "m/44'/60'/0'/0"
};
export const ETH_LEDGER: DPath = {
label: 'Ledger (ETH)',
value: "m/44'/60'/0'"
@ -23,6 +28,11 @@ export const ETC_TREZOR: DPath = {
value: "m/44'/61'/0'/0"
};
export const ETC_SAFE_T: DPath = {
label: 'Safe-T (ETC)',
value: "m/44'/61'/0'/0"
};
export const ETH_TESTNET: DPath = {
label: 'Testnet (ETH)',
value: "m/44'/1'/0'/0"
@ -83,6 +93,11 @@ export const RSK_TESTNET: DPath = {
value: "m/44'/37310'/0'/0"
};
export const RSK_MAINNET: DPath = {
label: 'Mainnet (RSK)',
value: "m/44'/137'/0'/0"
};
export const GO_DEFAULT: DPath = {
label: 'Default (GO)',
value: "m/44'/6060'/0'/0"
@ -101,9 +116,11 @@ export const ESN_DEFAULT: DPath = {
export const DPaths: DPath[] = [
ETH_DEFAULT,
ETH_TREZOR,
ETH_SAFE_T,
ETH_LEDGER,
ETC_LEDGER,
ETC_TREZOR,
ETC_SAFE_T,
ETH_TESTNET,
EXP_DEFAULT,
UBQ_DEFAULT,
@ -114,6 +131,7 @@ export const DPaths: DPath[] = [
ETSC_DEFAULT,
EGEM_DEFAULT,
CLO_DEFAULT,
RSK_MAINNET,
RSK_TESTNET,
GO_DEFAULT,
EOSC_DEFAULT,

View File

@ -3,6 +3,7 @@ import {
discordURL,
ledgerReferralURL,
trezorReferralURL,
safeTReferralURL,
ethercardReferralURL,
keepkeyReferralURL,
steelyReferralURL
@ -84,6 +85,10 @@ export const affiliateLinks: Link[] = [
link: trezorReferralURL,
text: translateRaw('TREZOR_REFERAL')
},
{
link: safeTReferralURL,
text: translateRaw('SAFE_T_REFERAL')
},
{
link: keepkeyReferralURL,
text: translateRaw('KEEPKEY_REFERRAL')

View File

@ -2310,7 +2310,7 @@
"decimal": 8
},
{
"address": "0xE94327D07Fc17907b4DB788E5aDf2ed424adDff6",
"address": "0x1985365e9f78359a9B6AD760e32412f4a445E862",
"symbol": "REP",
"decimal": 18
},
@ -2462,7 +2462,7 @@
{
"address": "0x12fCd6463E66974cF7bBC24FFC4d40d6bE458283",
"symbol": "GBX",
"decimal": 18
"decimal": 8
},
{
"address": "0x7728dFEF5aBd468669EB7f9b48A7f70a501eD29D",
@ -3328,5 +3328,32 @@
"address": "0xAc709FcB44a43c35F0DA4e3163b117A17F3770f5",
"symbol": "ARC",
"decimal": 18
},
{
"address": "0xDF2C7238198Ad8B389666574f2d8bc411A4b7428",
"symbol": "MFT",
"decimal": 18
},
{
"address": "0xc92d6e3e64302c59d734f3292e2a13a13d7e1817",
"symbol": "FXC",
"decimal": 8
},
{
"address": "0x4f3afec4e5a3f2a6a1a411def7d7dfe50ee057bf",
"symbol": "DGX 2.0",
"decimal": 9
},
{
"address": "0xd9a12cde03a86e800496469858de8581d3a5353d",
"symbol": "YUP",
"decimal": 18
},
{
"address": "0x6aEDbF8dFF31437220dF351950Ba2a3362168d1b",
"symbol": "DGS",
"decimal": 8
}
]
]

View File

@ -1 +1 @@
[]
[]

View File

@ -0,0 +1 @@
[]

View File

@ -1,27 +0,0 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.WelcomeSlide {
&-alert {
display: flex;
border-top: 2px solid color(brand-danger);
padding: $space-sm;
font-size: $font-size-base;
line-height: 1.5;
font-weight: 500;
box-shadow: 0 1px 1px 1px rgba(#000, 0.12);
margin-bottom: $space;
&-icon {
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
margin-left: $space * 0.4;
margin-right: $space * 0.8;
text-align: center;
font-size: 32px;
color: color(brand-danger);
}
}
}

View File

@ -2,31 +2,20 @@ import React from 'react';
import translate from 'translations';
import onboardIconOne from 'assets/images/onboarding/slide-01.svg';
import { Warning } from 'components/ui';
import OnboardSlide from './OnboardSlide';
import './WelcomeSlide.scss';
const WelcomeSlide = () => {
const header = translate('ONBOARD_WELCOME_TITLE');
const subheader = <small>{translate('ONBOARD_WELCOME_CONTENT__3')}</small>;
const content = (
<div>
<div className="WelcomeSlide-alert">
<div className="WelcomeSlide-alert-icon">
<i className="fa fa-exclamation-triangle" />
</div>
<span>
{translate('ONBOARD_WELCOME_CONTENT__1')}
{translate('ONBOARD_WELCOME_CONTENT__2')}
</span>
</div>
<div className="WelcomeSlide-alert">
<div className="WelcomeSlide-alert-icon">
<i className="fa fa-exclamation-triangle" />
</div>
{translate('ONBOARD_WELCOME_CONTENT__8')}
</div>
<Warning>
{translate('ONBOARD_WELCOME_CONTENT__1')}
{translate('ONBOARD_WELCOME_CONTENT__2')}
</Warning>
<Warning>{translate('ONBOARD_WELCOME_CONTENT__8')}</Warning>
<h5>{translate('ONBOARD_WELCOME_CONTENT__4')}</h5>
<ul>
<li>{translate('ONBOARD_WELCOME_CONTENT__5')}</li>

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { addHexPrefix } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import { AppState } from 'features/reducers';
import { paritySignerActions } from 'features/paritySigner';
@ -95,7 +95,7 @@ class QrSignerModal extends React.Component<Props, State> {
return;
}
this.props.finalizeSignature(signature);
this.props.finalizeSignature(addHexPrefix(signature));
this.setState({ scan: false });
};
}

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { AppState } from 'features/reducers';
import { getOffline } from 'features/config';
import { ElectronNav, AppAlphaNotice } from 'components';
import { ElectronNav } from 'components';
import OfflineTab from './OfflineTab';
import Notifications from './Notifications';
import './ElectronTemplate.scss';
@ -33,7 +33,6 @@ class ElectronTemplate extends Component<Props, {}> {
{isUnavailableOffline && isOffline ? <OfflineTab /> : children}
</div>
<Notifications />
<AppAlphaNotice />
</div>
<div className="ElectronTemplate-draggable" />
</div>

View File

@ -195,6 +195,12 @@ class InteractForm extends Component<Props, State> {
abiJson: fullContract.abi || '',
contract
});
} else {
this.props.setCurrentTo('');
this.setState({
abiJson: '',
contract
});
}
};

View File

@ -1,11 +1,12 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import translate from 'translations';
import translate, { translateRaw } from 'translations';
import { AppState } from 'features/reducers';
import * as selectors from 'features/selectors';
import { getOffline, getNetworkConfig } from 'features/config';
import { scheduleSelectors } from 'features/schedule';
import { notificationsActions } from 'features/notifications';
import {
AddressField,
AmountField,
@ -38,7 +39,26 @@ interface StateProps {
useScheduling: scheduleSelectors.ICurrentSchedulingToggle['value'];
}
class FieldsClass extends Component<StateProps> {
interface DispatchProps {
showNotification: notificationsActions.TShowNotification;
}
class FieldsClass extends Component<StateProps & DispatchProps> {
public componentDidCatch(error: Error) {
if (error.message === 'Serialized transaction not found') {
/**
* @desc Occasionally, when a new signed transaction matches a previous transaction,
* the nonce does not update, since the transaction has not yet been confirmed. This triggers
* the <Amounts /> component inside the <ConfirmationModal /> of <TXMetaDataPanel /> to throw
* an error when selecting the current transaction's serialized parameters.
* A longer term fix will involve finding a better way to calculate nonces to avoid
* nonce duplication on serial transactions.
*/
this.props.showNotification('danger', translateRaw('SIMILAR_TRANSACTION_ERROR'));
this.forceUpdate();
}
}
public render() {
const { shouldDisplay, schedulingAvailable, useScheduling } = this.props;
@ -54,7 +74,11 @@ class FieldsClass extends Component<StateProps> {
<div
className={schedulingAvailable ? 'col-sm-9 col-md-10' : 'col-sm-12 col-md-12'}
>
<AmountField hasUnitDropdown={true} hasSendEverything={true} />
<AmountField
hasUnitDropdown={true}
hasSendEverything={true}
showInvalidWithoutValue={true}
/>
</div>
{schedulingAvailable && (
<div className="col-sm-3 col-md-2">
@ -102,10 +126,15 @@ class FieldsClass extends Component<StateProps> {
}
}
export const Fields = connect((state: AppState) => ({
schedulingAvailable:
getNetworkConfig(state).name === 'Kovan' && selectors.getUnit(state) === 'ETH',
shouldDisplay: !selectors.isAnyOfflineWithWeb3(state),
offline: getOffline(state),
useScheduling: scheduleSelectors.getCurrentSchedulingToggle(state).value
}))(FieldsClass);
export const Fields = connect(
(state: AppState) => ({
schedulingAvailable:
getNetworkConfig(state).name === 'Kovan' && selectors.getUnit(state) === 'ETH',
shouldDisplay: !selectors.isAnyOfflineWithWeb3(state),
offline: getOffline(state),
useScheduling: scheduleSelectors.getCurrentSchedulingToggle(state).value
}),
{
showNotification: notificationsActions.showNotification
}
)(FieldsClass);

View File

@ -102,6 +102,7 @@ class RequestPayment extends React.Component<Props, {}> {
hasUnitDropdown={true}
showAllTokens={true}
customValidator={isValidAmount(decimal)}
showInvalidWithoutValue={true}
/>
</div>
</div>

View File

@ -18,6 +18,7 @@ interface Props {
signMessageRequested: messageActions.TSignMessageRequested;
signedMessage: ISignedMessage | null;
resetWallet: walletActions.TResetWallet;
resetMessage: messageActions.TResetMessage;
}
interface State {
@ -35,6 +36,7 @@ export class SignMessage extends Component<Props, State> {
public componentWillUnmount() {
this.props.resetWallet();
this.props.resetMessage();
}
public render() {
@ -97,6 +99,8 @@ export class SignMessage extends Component<Props, State> {
private changeWallet = () => {
this.props.resetWallet();
this.props.resetMessage();
this.setState(initialState);
};
}
@ -107,5 +111,6 @@ const mapStateToProps = (state: AppState) => ({
export default connect(mapStateToProps, {
signMessageRequested: messageActions.signMessageRequested,
resetWallet: walletActions.resetWallet
resetWallet: walletActions.resetWallet,
resetMessage: messageActions.resetMessage
})(SignMessage);

View File

@ -40,12 +40,13 @@
}
&-input {
color: #fff;
display: inline-block;
width: 16%;
min-width: 3.5rem;
height: 2rem;
padding: 1rem;
margin: .5rem auto;
margin: 0.5rem auto;
font-size: 1rem;
margin-right: $space;
text-align: right;

View File

@ -27,6 +27,8 @@ interface ReduxStateProps {
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
outputTx: any;
paymentId: string | null;
xmrPaymentAddress: string | null;
}
interface ReduxActionProps {
@ -68,6 +70,8 @@ export default class PartThree extends PureComponent<ReduxActionProps & ReduxSta
shapeshiftOrderStatus,
destinationAddress,
outputTx,
paymentId,
xmrPaymentAddress,
// ACTIONS
showNotificationWithComponent
} = this.props;
@ -85,7 +89,9 @@ export default class PartThree extends PureComponent<ReduxActionProps & ReduxSta
const PaymentInfoProps = {
origin,
paymentAddress
paymentAddress,
paymentId,
xmrPaymentAddress
};
const BitcoinQRProps = {

View File

@ -1,18 +1,31 @@
@import "common/sass/variables";
@import "common/sass/mixins";
@import 'common/sass/variables';
@import 'common/sass/mixins';
.SwapPayment {
text-align: center;
margin-bottom: $space;
> * {
margin: $space auto 0;
width: 100%;
max-width: 620px;
}
h2 {
text-align: center;
& > input {
margin-top: $space;
}
}
h4 {
margin: 0.5rem 0;
}
&-address {
@include mono;
display: block;
margin: $space auto 0;
max-width: 620px;
width: 100%;
font-size: $font-size-medium;
text-align: center;
@include mono;
}
@media screen and (max-width: $screen-sm) {
@ -20,4 +33,16 @@
font-size: $font-size-base;
}
}
&-payment-id {
h2 {
font-weight: bolder;
text-align: center;
}
&-link {
font-size: $font-size-small;
text-align: right;
}
}
}

View File

@ -2,19 +2,47 @@ import React, { PureComponent } from 'react';
import translate from 'translations';
import { SwapInput } from 'features/swap/types';
import { Input } from 'components/ui';
import { Input, Warning } from 'components/ui';
import './PaymentInfo.scss';
export interface Props {
origin: SwapInput;
paymentAddress: string | null;
/**
* @desc
* For XMR swaps, the "deposit" property in the response
* actually refers to the "paymentId", not the payment address.
*/
paymentId: string | null;
/**
* @desc
* For XMR swap, the actual payment address is the "sAddress"
* property in the response.
*/
xmrPaymentAddress: string | null;
}
export default class PaymentInfo extends PureComponent<Props, {}> {
public render() {
const { origin } = this.props;
const { origin, paymentAddress, paymentId, xmrPaymentAddress } = this.props;
const isXMRSwap = origin.label === 'XMR';
const actualPaymentAddress = isXMRSwap ? xmrPaymentAddress : paymentAddress;
return (
<section className="SwapPayment">
{isXMRSwap && (
<section className="SwapPayment-payment-id">
<h2>
{translate('USING_PAYMENT_ID')}
<Input
className="SwapPayment-address"
isValid={!!paymentId}
value={paymentId || undefined}
disabled={true}
/>
</h2>
</section>
)}
<h2>
{translate('SWAP_SEND_TO', {
$origin_amount: origin.amount.toString(),
@ -22,11 +50,16 @@ export default class PaymentInfo extends PureComponent<Props, {}> {
})}
<Input
className="SwapPayment-address"
isValid={!!this.props.paymentAddress}
value={this.props.paymentAddress || undefined}
isValid={!!actualPaymentAddress}
value={actualPaymentAddress || undefined}
disabled={true}
/>
</h2>
{isXMRSwap && (
<Warning highlighted={true}>
<h4>{translate('PAYMENT_ID_WARNING')}</h4>
</Warning>
)}
</section>
);
}

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import { donationAddressMap } from 'config';
import { donationAddressMap, WhitelistedCoins } from 'config';
import translate, { translateRaw } from 'translations';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import { isValidBTCAddress, isValidETHAddress, isValidXMRAddress } from 'libs/validators';
import { combineAndUpper } from 'utils/formatters';
import { SwapInput } from 'features/swap/types';
import {
@ -18,7 +18,7 @@ import './ReceivingAddress.scss';
export interface StateProps {
origin: SwapInput;
destinationId: keyof typeof donationAddressMap;
destinationId: WhitelistedCoins;
isPostingOrder: boolean;
destinationAddress: string;
destinationKind: number;
@ -62,13 +62,22 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
public render() {
const { destinationId, destinationAddress, isPostingOrder } = this.props;
let validAddress;
// TODO - find better pattern here once currencies move beyond BTC, ETH, REP
if (destinationId === 'BTC') {
validAddress = isValidBTCAddress(destinationAddress);
} else {
validAddress = isValidETHAddress(destinationAddress);
}
const addressValidators: { [coinOrToken: string]: (address: string) => boolean } = {
BTC: isValidBTCAddress,
XMR: isValidXMRAddress,
ETH: isValidETHAddress
};
// If there is no matching validator for the ID, assume it's a token and use ETH.
const addressValidator = addressValidators[destinationId] || addressValidators.ETH;
const validAddress = addressValidator(destinationAddress);
const placeholders: { [coinOrToken: string]: string } = {
BTC: donationAddressMap.BTC,
XMR: donationAddressMap.XMR,
ETH: donationAddressMap.ETH
};
const placeholder = placeholders[destinationId] || donationAddressMap.ETH;
return (
<section className="SwapAddress block">
@ -85,11 +94,7 @@ export default class ReceivingAddress extends PureComponent<StateProps & ActionP
type="text"
value={destinationAddress}
onChange={this.onChangeDestinationAddress}
placeholder={
destinationId === 'BTC'
? donationAddressMap[destinationId]
: donationAddressMap.ETH
}
placeholder={placeholder}
/>
</label>
</div>

View File

@ -34,6 +34,8 @@ interface ReduxStateProps {
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
paymentAddress: string | null;
paymentId: string | null;
xmrPaymentAddress: string | null;
isOffline: boolean;
}
@ -77,6 +79,8 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps & RouteComponent
shapeshiftOrderStatus,
isPostingOrder,
outputTx,
paymentId,
xmrPaymentAddress,
// ACTIONS
initSwap,
restartSwap,
@ -159,7 +163,9 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps & RouteComponent
stopPollShapeshiftOrderStatus,
showNotificationWithComponent,
destinationAddress,
outputTx
outputTx,
paymentId,
xmrPaymentAddress
};
const SupportProps = {
@ -220,6 +226,8 @@ function mapStateToProps(state: AppState) {
bityOrderStatus: state.swap.bityOrderStatus,
shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
paymentAddress: state.swap.paymentAddress,
paymentId: state.swap.paymentId,
xmrPaymentAddress: state.swap.xmrPaymentAddress,
isOffline: getOffline(state)
};
}

View File

@ -12,7 +12,9 @@ Object {
}
`;
exports[`handleChangeNodeRequested* should select getCustomNodeConfig and match race snapshot 1`] = `
exports[
`handleChangeNodeRequested* should select getCustomNodeConfig and match race snapshot 1`
] = `
Object {
"@@redux-saga/IO": true,
"SELECT": Object {

View File

@ -9,10 +9,12 @@ import {
ELLA_DEFAULT,
ETC_LEDGER,
ETC_TREZOR,
ETC_SAFE_T,
ETH_DEFAULT,
ETH_LEDGER,
ETH_TESTNET,
ETH_TREZOR,
ETH_SAFE_T,
EXP_DEFAULT,
POA_DEFAULT,
TOMO_DEFAULT,
@ -21,6 +23,7 @@ import {
ETSC_DEFAULT,
EGEM_DEFAULT,
CLO_DEFAULT,
RSK_MAINNET,
RSK_TESTNET,
GO_DEFAULT,
EOSC_DEFAULT,
@ -28,6 +31,7 @@ import {
} from 'config/dpaths';
import { makeExplorer } from 'utils/helpers';
import { StaticNetworksState } from './types';
import { TAB } from 'components/Header/components/constants';
const testnetDefaultGasPrice = {
min: 0.1,
@ -55,6 +59,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: require('config/contracts/eth.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TREZOR,
[SecureWalletName.SAFE_T]: ETH_SAFE_T,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
},
@ -77,6 +82,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.SAFE_T]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
},
@ -98,6 +104,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.SAFE_T]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
},
@ -119,6 +126,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.SAFE_T]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
},
@ -140,6 +148,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: require('config/contracts/etc.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETC_TREZOR,
[SecureWalletName.SAFE_T]: ETC_SAFE_T,
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
},
@ -164,6 +173,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: require('config/contracts/ubq.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
[SecureWalletName.SAFE_T]: UBQ_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
},
@ -188,6 +198,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: require('config/contracts/exp.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: EXP_DEFAULT,
[SecureWalletName.SAFE_T]: EXP_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
},
@ -205,7 +216,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
isCustom: false,
color: '#6d2eae',
blockExplorer: makeExplorer({
name: 'Etherchain Light',
name: 'POA Explorer',
origin: 'https://poaexplorer.com',
addressPath: 'address/search',
blockPath: 'blocks/block'
@ -214,6 +225,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: POA_DEFAULT,
[SecureWalletName.SAFE_T]: POA_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: POA_DEFAULT
},
@ -239,6 +251,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
dPathFormats: {
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[SecureWalletName.TREZOR]: ETH_TREZOR,
[SecureWalletName.SAFE_T]: ETH_SAFE_T,
[SecureWalletName.LEDGER_NANO_S]: TOMO_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: TOMO_DEFAULT
},
@ -263,6 +276,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: ELLA_DEFAULT,
[SecureWalletName.SAFE_T]: ELLA_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: ELLA_DEFAULT
},
gasPriceSettings: {
@ -288,6 +302,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: MUSIC_DEFAULT,
[SecureWalletName.SAFE_T]: MUSIC_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: MUSIC_DEFAULT
},
gasPriceSettings: {
@ -312,6 +327,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: ETSC_DEFAULT,
[SecureWalletName.SAFE_T]: ETSC_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: ETSC_DEFAULT
},
gasPriceSettings: {
@ -336,6 +352,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: EGEM_DEFAULT,
[SecureWalletName.SAFE_T]: EGEM_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EGEM_DEFAULT
},
gasPriceSettings: {
@ -360,6 +377,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: CLO_DEFAULT,
[SecureWalletName.SAFE_T]: CLO_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: CLO_DEFAULT
},
gasPriceSettings: {
@ -369,6 +387,33 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
}
},
RSK: {
id: 'RSK',
name: 'RSK',
unit: 'SBTC',
chainId: 30,
color: '#58A052',
isCustom: false,
blockExplorer: makeExplorer({
name: 'RSK Explorer',
origin: 'https://explorer.rsk.co'
}),
tokens: require('config/tokens/rsk.json'),
contracts: require('config/contracts/rsk.json'),
isTestnet: false,
dPathFormats: {
[SecureWalletName.TREZOR]: RSK_MAINNET,
[SecureWalletName.LEDGER_NANO_S]: RSK_MAINNET,
[InsecureWalletName.MNEMONIC_PHRASE]: RSK_MAINNET
},
gasPriceSettings: {
min: 0.183,
max: 1.5,
initial: 0.183
},
unsupportedTabs: [TAB.ENS]
},
RSK_TESTNET: {
id: 'RSK_TESTNET',
name: 'RSK',
@ -380,11 +425,12 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
name: 'RSK Testnet Explorer',
origin: 'https://explorer.testnet.rsk.co'
}),
tokens: require('config/tokens/rsk.json'),
contracts: require('config/contracts/rsk.json'),
tokens: require('config/tokens/rsk_testnet.json'),
contracts: require('config/contracts/rsk_testnet.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: RSK_TESTNET,
[SecureWalletName.SAFE_T]: RSK_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: RSK_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: RSK_TESTNET
},
@ -392,7 +438,8 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
min: 0.183,
max: 1.5,
initial: 0.183
}
},
unsupportedTabs: [TAB.ENS]
},
GO: {
@ -410,6 +457,33 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: GO_DEFAULT,
[SecureWalletName.SAFE_T]: GO_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: GO_DEFAULT
},
gasPriceSettings: {
min: 2,
max: 60,
initial: 2
}
},
GO_TESTNET: {
id: 'GO_TESTNET',
name: 'GO',
unit: 'GO',
chainId: 31337,
isCustom: false,
color: '#00b04a',
blockExplorer: makeExplorer({
name: 'GoChain Testnet Explorer',
origin: 'https://testnet-explorer.gochain.io'
}),
tokens: [],
contracts: [],
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: GO_DEFAULT,
[SecureWalletName.SAFE_T]: GO_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: GO_DEFAULT
},
gasPriceSettings: {
@ -434,6 +508,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: [],
dPathFormats: {
[SecureWalletName.TREZOR]: EOSC_DEFAULT,
[SecureWalletName.SAFE_T]: EOSC_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EOSC_DEFAULT
},
gasPriceSettings: {
@ -442,6 +517,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
initial: 20
}
},
ESN: {
id: 'ESN',
name: 'EthersocialNetwork',
@ -457,6 +533,7 @@ export const STATIC_NETWORKS_INITIAL_STATE: StaticNetworksState = {
contracts: require('config/contracts/esn.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ESN_DEFAULT,
[SecureWalletName.SAFE_T]: ESN_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: ESN_DEFAULT
},
gasPriceSettings: {

View File

@ -158,6 +158,7 @@ export function isWalletFormatSupportedOnNetwork(state: AppState, format: Wallet
const CHECK_FORMATS: DPathFormat[] = [
SecureWalletName.LEDGER_NANO_S,
SecureWalletName.TREZOR,
SecureWalletName.SAFE_T,
InsecureWalletName.MNEMONIC_PHRASE
];

View File

@ -27,5 +27,6 @@ export type ConfigAction = CustomNetworkAction | CustomNodeAction | NodeAction |
export type DPathFormat =
| SecureWalletName.TREZOR
| SecureWalletName.SAFE_T
| SecureWalletName.LEDGER_NANO_S
| InsecureWalletName.MNEMONIC_PHRASE;

View File

@ -1,6 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getDeterministicWallets* starting from publicKey & chainCode should match put snapshot 1`] = `
exports[
`getDeterministicWallets* starting from publicKey & chainCode should match put snapshot 1`
] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
@ -215,4 +217,4 @@ Object {
},
],
}
`;
`;

View File

@ -0,0 +1,108 @@
import noop from 'lodash/noop';
import handleMetaMaskPolling, { getActualChainId } from './handleMetaMaskPolling';
import * as configNetworksSelectors from './config/networks/selectors';
import { walletSelectors } from './wallet';
jest.mock('./config/networks/selectors');
jest.mock('./wallet');
describe('getActualChainId', () => {
it('should reject with an error if web3 does not exist', async done => {
try {
await getActualChainId();
} catch (e) {
expect(e).toBe('Web3 not found.');
done();
}
});
it('should reject with an error web3 if fails its network check', async done => {
(global as any).web3 = {
version: {
getNetwork: jest.fn(callback => callback('Network check failed'))
}
};
try {
await getActualChainId();
} catch (e) {
expect(e).toBe('Network check failed');
done();
}
});
it('should return a chainId when everything is fine', async done => {
(global as any).web3 = {
version: {
getNetwork: jest.fn(callback => callback(null, '1'))
}
};
const network = await getActualChainId();
expect(network).toBe('1');
done();
});
});
describe('handleMetaMaskPolling', () => {
it('should do nothing when there is no wallet instance', async done => {
(global as any).web3 = {
version: {
getNetwork: jest.fn(callback => callback(null, '1'))
}
};
(walletSelectors as any).getWalletInst.mockReturnValue(null);
(configNetworksSelectors as any).getNetworkByChainId.mockReturnValue('ETH');
const store = {
getState: noop,
dispatch: noop
};
const result = await handleMetaMaskPolling(store as any);
expect(result).toBe(false);
done();
});
it('should reload the page if the network has changed', async done => {
(global as any).web3 = {
version: {
getNetwork: jest.fn(callback => callback(null, '1'))
}
};
(walletSelectors as any).getWalletInst.mockReturnValue({
network: 'ETC'
});
(configNetworksSelectors as any).getNetworkByChainId.mockReturnValue('ETH');
const store = {
getState: noop
};
const result = await handleMetaMaskPolling(store as any);
expect(result).toBe(true);
done();
});
it('should reload the page if `getActualChainId` rejects', async done => {
(global as any).web3 = {
version: {
getNetwork: jest.fn(callback => callback('Network check failed'))
}
};
(walletSelectors as any).getWalletInst.mockReturnValue({
network: 'ETH'
});
(configNetworksSelectors as any).getNetworkByChainId.mockReturnValue('ETH');
const store = {
getState: noop
};
const result = await handleMetaMaskPolling(store as any);
expect(result).toBe(true);
done();
});
});

View File

@ -0,0 +1,57 @@
import { Store } from 'redux';
import { Web3Wallet } from 'libs/wallet';
import { AppState } from './reducers';
import * as configNetworksSelectors from './config/networks/selectors';
import { walletSelectors } from './wallet';
export const METAMASK_POLLING_INTERVAL: number = 1000;
export const getActualChainId = (): Promise<string> =>
new Promise((resolve, reject) => {
const { web3 } = window as any;
if (!web3) {
reject('Web3 not found.');
}
return web3.version.getNetwork(
(err: Error, network: string) => (err ? reject(err) : resolve(network))
);
});
/**
* @desc
* MetaMask no longer refreshes the page automatically on network change,
* so we must poll to ensure the network is the same as the locally stored version.
* @see https://medium.com/metamask/breaking-change-no-longer-reloading-pages-on-network-change-4a3e1fd2f5e7
*/
export default async function handleMetaMaskPolling(store: Store<AppState>): Promise<boolean> {
const state = store.getState();
try {
// Locally stored network.
const web3Wallet = walletSelectors.getWalletInst(state);
// MetaMask's actual network.
const actualChainId = await getActualChainId();
const actualNetwork = configNetworksSelectors.getNetworkByChainId(state, actualChainId);
if (
web3Wallet &&
(web3Wallet as Web3Wallet).network &&
actualNetwork &&
(web3Wallet as Web3Wallet).network !== actualNetwork.id
) {
window.location.reload();
return true;
}
} catch (error) {
window.location.reload();
return true;
}
return false;
}

View File

@ -25,3 +25,10 @@ export function signMessageFailed(): types.SignMessageFailedAction {
type: types.MessageActions.SIGN_FAILED
};
}
export type TResetMessage = typeof resetMessage;
export function resetMessage(): types.ResetMessageAction {
return {
type: types.MessageActions.RESET
};
}

View File

@ -21,6 +21,12 @@ function signMessageFailed(state: types.MessageState): types.MessageState {
};
}
function resetMessage(): types.MessageState {
return {
...INITIAL_STATE
};
}
export function messageReducer(
state: types.MessageState = INITIAL_STATE,
action: types.MessageAction
@ -30,6 +36,8 @@ export function messageReducer(
return signLocalMessageSucceeded(state, action);
case types.MessageActions.SIGN_FAILED:
return signMessageFailed(state);
case types.MessageActions.RESET:
return resetMessage();
default:
return state;
}

View File

@ -3,7 +3,8 @@ import { ISignedMessage } from 'libs/signing';
export enum MessageActions {
SIGN_REQUESTED = 'MESSAGE_SIGN_REQUESTED',
SIGN_LOCAL_SUCCEEDED = 'MESSAGE_SIGN_LOCAL_SUCCEEDED',
SIGN_FAILED = 'MESSAGE_SIGN_FAILED'
SIGN_FAILED = 'MESSAGE_SIGN_FAILED',
RESET = 'MESSAGE_RESET'
}
export interface MessageState {
@ -24,7 +25,12 @@ export interface SignMessageFailedAction {
type: MessageActions.SIGN_FAILED;
}
export interface ResetMessageAction {
type: MessageActions.RESET;
}
export type MessageAction =
| SignMessageRequestedAction
| SignLocalMessageSucceededAction
| SignMessageFailedAction;
| SignMessageFailedAction
| ResetMessageAction;

View File

@ -69,7 +69,7 @@ export function getDisabledWallets(state: AppState): any {
// Some wallets are unavailable offline
if (isOffline) {
addReason(
[SecureWalletName.WEB3, SecureWalletName.TREZOR],
[SecureWalletName.WEB3, SecureWalletName.TREZOR, SecureWalletName.SAFE_T],
'This wallet cannot be accessed offline'
);
}
@ -77,6 +77,10 @@ export function getDisabledWallets(state: AppState): any {
// Some wallets are disabled on certain platforms
if (process.env.BUILD_ELECTRON) {
addReason([SecureWalletName.WEB3], 'This wallet is not supported in the MyCrypto app');
addReason(
[SecureWalletName.SAFE_T],
'Coming soon. Please use the MyCrypto.com website in the meantime'
);
}
// Dedupe and sort for consistency

View File

@ -3,6 +3,7 @@ import { bindActionCreators } from 'redux';
import { shepherdProvider, getShepherdPending, getShepherdOffline } from 'libs/nodes';
import { setOffline, setOnline, getOffline } from 'features/config';
import { notificationsActions } from 'features/notifications';
import handleMetaMaskPolling, { METAMASK_POLLING_INTERVAL } from './handleMetaMaskPolling';
import configureStore from './configureStore';
const store = configureStore();
@ -90,4 +91,9 @@ window.addEventListener('load', () => {
});
});
/** @desc When MetaMask is loaded as an extension, watch for network changes. */
if ((window as any).web3) {
setInterval(handleMetaMaskPolling.bind(null, store), METAMASK_POLLING_INTERVAL);
}
export default store;

View File

@ -234,7 +234,8 @@ describe('swap reducer', () => {
maxLimit: 7.04575258,
apiPubKey:
'0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160',
minerFee: '1.05'
minerFee: '1.05',
sAddress: '0x055ed77933388642fdn4px9v73j4fa3582d10c4'
};
const swapState = reducer.swapReducer(
@ -254,8 +255,10 @@ describe('swap reducer', () => {
validFor: swapState.validFor,
orderTimestampCreatedISOString: swapState.orderTimestampCreatedISOString,
paymentAddress: mockedShapeshiftOrder.deposit,
paymentId: mockedShapeshiftOrder.deposit,
shapeshiftOrderStatus: 'no_deposits',
orderId: mockedShapeshiftOrder.orderId
orderId: mockedShapeshiftOrder.orderId,
xmrPaymentAddress: mockedShapeshiftOrder.sAddress
});
});

View File

@ -42,7 +42,9 @@ export const INITIAL_STATE: types.SwapState = {
paymentAddress: null,
validFor: null,
orderId: null,
showLiteSend: false
showLiteSend: false,
paymentId: null,
xmrPaymentAddress: null
};
export function swapReducer(state: types.SwapState = INITIAL_STATE, action: types.SwapAction) {
@ -69,22 +71,19 @@ export function swapReducer(state: types.SwapState = INITIAL_STATE, action: type
isFetchingRates: false
};
case types.SwapActions.LOAD_SHAPESHIFT_RATES_SUCCEEDED:
const {
entities: { providerRates: normalizedProviderRates, options: normalizedOptions }
} = normalize(action.payload, [providerRate]);
return {
...state,
shapeshiftRates: {
byId: normalize(action.payload, [providerRate]).entities.providerRates,
allIds: allIds(normalize(action.payload, [providerRate]).entities.providerRates)
byId: normalizedProviderRates,
allIds: allIds(normalizedProviderRates)
},
options: {
byId: Object.assign(
{},
normalize(action.payload, [providerRate]).entities.options,
state.options.byId
),
allIds: [
...allIds(normalize(action.payload, [providerRate]).entities.options),
...state.options.allIds
]
byId: { ...normalizedOptions, ...state.options.byId },
allIds: [...allIds(normalizedOptions), ...state.options.allIds]
},
isFetchingRates: false
};
@ -151,8 +150,8 @@ export function swapReducer(state: types.SwapState = INITIAL_STATE, action: type
};
case types.SwapActions.SHAPESHIFT_ORDER_CREATE_SUCCEEDED:
const currDate = Date.now();
const secondsRemaining = Math.floor((+new Date(action.payload.expiration) - currDate) / 1000);
return {
...state,
shapeshiftOrder: {
@ -166,7 +165,10 @@ export function swapReducer(state: types.SwapState = INITIAL_STATE, action: type
orderTimestampCreatedISOString: new Date(currDate).toISOString(),
paymentAddress: action.payload.deposit,
shapeshiftOrderStatus: 'no_deposits',
orderId: action.payload.orderId
orderId: action.payload.orderId,
// For XMR swaps
paymentId: action.payload.deposit,
xmrPaymentAddress: action.payload.sAddress
};
case types.SwapActions.BITY_ORDER_STATUS_SUCCEEDED:
return {

View File

@ -24,6 +24,18 @@ export interface SwapState {
validFor: number | null;
orderId: string | null;
showLiteSend: boolean;
/**
* @desc
* For XMR swaps, the "deposit" property in the response
* actually refers to the "paymentId", not the payment address.
*/
paymentId: string | null;
/**
* @desc
* For XMR swap, the actual payment address is the "sAddress"
* property in the response.
*/
xmrPaymentAddress: string | null;
}
export enum SwapActions {
@ -208,6 +220,7 @@ export interface ShapeshiftOrderResponse {
quotedRate: string;
withdrawal: string;
withdrawalAmount: string;
sAddress?: string;
}
export interface ShapeshiftStatusResponse {

View File

@ -3,6 +3,7 @@ import { WalletConfig } from 'libs/wallet/config';
import { IWallet } from 'libs/wallet/IWallet';
import { LedgerWallet } from 'libs/wallet/deterministic/ledger';
import { TrezorWallet } from 'libs/wallet/deterministic/trezor';
import { SafeTWallet } from 'libs/wallet/deterministic/safe-t';
import Web3Wallet from 'libs/wallet/non-deterministic/web3';
import ParitySignerWallet from 'libs/wallet/non-deterministic/parity';
import { AppState } from 'features/reducers';
@ -32,8 +33,9 @@ export const getWalletType = (state: AppState): IWalletType => {
const isWeb3Wallet = wallet instanceof Web3Wallet;
const isLedgerWallet = wallet instanceof LedgerWallet;
const isTrezorWallet = wallet instanceof TrezorWallet;
const isSafeTWallet = wallet instanceof SafeTWallet;
const isParitySignerWallet = wallet instanceof ParitySignerWallet;
const isHardwareWallet = isLedgerWallet || isTrezorWallet;
const isHardwareWallet = isLedgerWallet || isTrezorWallet || isSafeTWallet;
return { isWeb3Wallet, isHardwareWallet, isParitySignerWallet };
};

View File

@ -4,6 +4,7 @@
<head>
<meta charset="utf-8">
<title>MyCrypto</title>
<meta http-equiv="Content-Security-Policy" content="<%= htmlWebpackPlugin.options.metaCsp %>" >
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="MyCrypto is a free, open-source interface for interacting with the blockchain.">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
@ -12,7 +13,7 @@
<meta property="og:description" content="<%= htmlWebpackPlugin.options.appDescription %>">
<meta property="og:site_name" content="<%= htmlWebpackPlugin.options.title %>">
<meta property="og:type" content="<%= htmlWebpackPlugin.options.type %>">
<meta property="og:image" content="<%= htmlWebpackPlugin.options.image %>">
<meta property="og:image" content="/common/assets/images/link-preview.png">
<meta name="twitter:site" content="<%= htmlWebpackPlugin.options.twitter.site %>">
<meta name="twitter:creator" content="<%= htmlWebpackPlugin.options.twitter.creator %>">
<meta name="google-site-verification" content="dRWkvANAUNAhNyMnTyc7M7S3lnucotMY8j8R-gsZhbo" />
@ -71,40 +72,6 @@
</div>
</div>
<script>
(function () {
var badBrowser = false;
try {
// Let and const
eval('let a = 1;');
eval('const b = 1');
// Local storage
window.localStorage.setItem('test', 'test');
window.localStorage.removeItem('test');
// Flexbox
var el = document.createElement('div');
el.style.display = 'flex';
if (el.style.display !== 'flex') {
badBrowser = false;
}
} catch (err) {
badBrowser = true;
}
if (badBrowser) {
var el = document.getElementsByClassName('BadBrowser')[0];
el.className += ' is-open';
// Dumb check for known mobile OS's. Not important to catch all, just
// displays more appropriate information.
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
el.className += ' is-mobile';
}
}
})();
</script>
</body>
</html>
</html>

View File

@ -27,3 +27,12 @@ if (module.hot) {
if (process.env.NODE_ENV === 'production') {
consoleAdvertisement();
}
const noOp = (event: DragEvent) => {
event.preventDefault();
return false;
};
// disables drag-and-drop due to potential security issues by Cure53 recommendation
document.addEventListener('dragover', noOp, false);
document.addEventListener('drop', noOp, false);

View File

@ -102,9 +102,9 @@ export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = {
POA: [
{
name: makeNodeName('POA', 'core'),
type: 'rpc',
service: 'poa.network',
url: 'https://core.poa.network'
type: 'infura',
service: 'poa.infura.io',
url: 'https://poa.infura.io'
}
],
@ -168,6 +168,15 @@ export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = {
}
],
RSK: [
{
name: makeNodeName('RSK', 'rsk_mainnet'),
type: 'rpc',
service: 'mycrypto.rsk.co',
url: 'https://mycrypto.rsk.co/'
}
],
RSK_TESTNET: [
{
name: makeNodeName('RSK_TESTNET', 'rsk_testnet'),
@ -186,6 +195,15 @@ export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = {
}
],
GO_TESTNET: [
{
name: makeNodeName('GO_TESTNET', 'go_testnet'),
type: 'rpc',
service: 'testnet-rpc.gochain.io',
url: 'https://testnet-rpc.gochain.io/'
}
],
EOSC: [
{
name: makeNodeName('EOSC', 'eosc'),

View File

@ -59,6 +59,12 @@ export function isValidBTCAddress(address: string): boolean {
return WalletAddressValidator.validate(address, 'BTC');
}
export function isValidXMRAddress(address: string): boolean {
return !!address.match(
/4[0-9AB][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{93}/
);
}
export function isValidHex(str: string): boolean {
if (str === '') {
return true;
@ -133,11 +139,20 @@ export const validPositiveNumber = (num: number) => validNumber(num) && num !==
export const validDecimal = (input: string, decimal: number) => {
const arr = input.split('.');
// Only a single decimal can exist.
if (arr.length > 2) {
return false;
}
const fractionPortion = arr[1];
if (!fractionPortion || fractionPortion.length === 0) {
return true;
}
const decimalLength = fractionPortion.length;
return decimalLength <= decimal;
};

View File

@ -9,6 +9,7 @@ import { HardwareWallet, ChainCodeResponse } from './hardware';
const walletTypeNames = {
[WalletTypes.LEDGER]: 'X_LEDGER',
[WalletTypes.TREZOR]: 'X_TREZOR',
[WalletTypes.SAFE_T]: 'X_SAFE_T',
[WalletTypes.KEEPKEY]: 'X_KEEPKEY'
};

View File

@ -2,6 +2,7 @@ import { WalletTypes } from 'shared/enclave/client';
import { makeEnclaveWallet } from './enclave';
import { LedgerWallet as LedgerWalletWeb } from './ledger';
import { TrezorWallet as TrezorWalletWeb } from './trezor';
import { SafeTWallet as SafeTWalletWeb } from './safe-t';
function enclaveOrWallet<T>(type: WalletTypes, lib: T) {
return process.env.BUILD_ELECTRON ? makeEnclaveWallet(type) : lib;
@ -11,3 +12,4 @@ export * from './mnemonic';
export * from './hardware';
export const LedgerWallet = enclaveOrWallet(WalletTypes.LEDGER, LedgerWalletWeb);
export const TrezorWallet = enclaveOrWallet(WalletTypes.TREZOR, TrezorWalletWeb);
export const SafeTWallet = enclaveOrWallet(WalletTypes.SAFE_T, SafeTWalletWeb);

View File

@ -0,0 +1,116 @@
import BN from 'bn.js';
import EthTx, { TxObj } from 'ethereumjs-tx';
import { addHexPrefix } from 'ethereumjs-util';
import mapValues from 'lodash/mapValues';
import { translateRaw } from 'translations';
import SafeTConnect from 'vendor/safe-t-connect';
import { getTransactionFields } from 'libs/transaction';
import { padLeftEven } from 'libs/values';
import { stripHexPrefixAndLower } from 'libs/formatters';
import { HardwareWallet, ChainCodeResponse } from './hardware';
export const SAFE_T_MINIMUM_FIRMWARE = '1.0.0';
export class SafeTWallet extends HardwareWallet {
public static getChainCode(dpath: string): Promise<ChainCodeResponse> {
return new Promise(resolve => {
SafeTConnect.getXPubKey(
dpath,
res => {
if (res.success) {
resolve({
publicKey: res.publicKey,
chainCode: res.chainCode
});
} else {
throw new Error(res.error);
}
},
SAFE_T_MINIMUM_FIRMWARE
);
});
}
public signRawTransaction(tx: EthTx): Promise<Buffer> {
return new Promise((resolve, reject) => {
const { chainId, ...strTx } = getTransactionFields(tx);
// stripHexPrefixAndLower identical to ethFuncs.getNakedAddress
const cleanedTx = mapValues(mapValues(strTx, stripHexPrefixAndLower), padLeftEven);
SafeTConnect.ethereumSignTx(
// Args
this.getPath(),
cleanedTx.nonce,
cleanedTx.gasPrice,
cleanedTx.gasLimit,
cleanedTx.to,
cleanedTx.value,
cleanedTx.data,
chainId,
// Callback
result => {
if (!result.success) {
return reject(Error(result.error));
}
// TODO: Explain what's going on here? Add tests? Adapted from:
// https://github.com/kvhnuke/etherwallet/blob/v3.10.2.6/app/scripts/uiFuncs.js#L24
const txToSerialize: TxObj = {
...strTx,
v: addHexPrefix(new BN(result.v).toString(16)),
r: addHexPrefix(result.r.toString()),
s: addHexPrefix(result.s)
};
const eTx = new EthTx(txToSerialize);
const serializedTx = eTx.serialize();
resolve(serializedTx);
}
);
});
}
public signMessage() {
return Promise.reject(new Error('Signing via Safe-T mini not yet supported.'));
}
public displayAddress(): Promise<boolean> {
return new Promise(resolve => {
SafeTConnect.ethereumGetAddress(
`${this.dPath}/${this.index}`,
res => {
if (res.error) {
resolve(false);
} else {
resolve(true);
}
},
SAFE_T_MINIMUM_FIRMWARE
);
});
}
public getWalletType(): string {
return translateRaw('X_SAFE_T');
}
// works, but returns a signature that can only be verified with a Safe-T mini device
/*
public signMessage = (message: string): Promise<string> => {
return new Promise((resolve, reject) => {
SafeTConnect.ethereumSignMessage(
this.getPath(),
message,
response => {
if (response.success) {
resolve(addHexPrefix(response.signature))
} else{
console.error(response.error)
reject(response.error)
}
}
)
})
}
*/
}

View File

@ -6,8 +6,9 @@ import { INode } from 'libs/nodes/INode';
import { IFullWallet } from '../IWallet';
export default class Web3Wallet implements IFullWallet {
public network: string;
private address: string;
private network: string;
constructor(address: string, network: string) {
this.address = address;

View File

@ -67,13 +67,21 @@ export function translateRaw(key: string, variables?: { [name: string]: string }
const translatedString =
(repository[language] && repository[language][key]) || repository[fallbackLanguage][key] || key;
if (!!variables) {
// Find each variable and replace it in the translated string
let str = translatedString;
Object.keys(variables).forEach(v => {
const re = new RegExp(v.replace('$', '\\$'), 'g');
str = str.replace(re, variables[v]);
/** @desc In RegExp, $foo is two "words", but __foo is only one "word."
* Replace all occurences of '$' with '__' in the entire string and each variable,
* then iterate over each variable, replacing the '__variable' in the
* translation key with the variable's value.
*/
if (variables) {
let str = translatedString.replace(/\$/g, '__');
Object.keys(variables).forEach(variable => {
const singleWordVariable = variable.replace(/\$/g, '__');
const re = new RegExp(`\\b${singleWordVariable}\\b`, 'g');
str = str.replace(re, variables[variable]);
});
return str;
}

View File

@ -282,6 +282,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Connexion au TREZOR ",
"ADD_TREZOR_SELECT": "Ceci est une _seed_ TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connexion au Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Réouvrir MyCrypto sur une connexion sécurisée (SSL) ",
"ADD_DIGITALBITBOX_0B": "Réouvrir MyCrypto avec [Chrome](https://www.google.com/chrome/browser/desktop/) ou [Opera](https://www.opera.com/) ",

View File

@ -295,6 +295,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Zu TREZOR Verbinden ",
"ADD_TREZOR_SELECT": "Dies ist ein TREZOR seed ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Zu Safe-T mini Verbinden ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Re-open MyCrypto on a secure (SSL) connection ",
"ADD_DIGITALBITBOX_0B": "Re-open MyCrypto using [Chrome](https://www.google.com/chrome/browser/desktop/) or [Opera](https://www.opera.com/) ",

View File

@ -296,6 +296,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Συνδεθείτε στο TREZOR ",
"ADD_TREZOR_SELECT": "Αυτός είναι σπόρος του TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Συνδεθείτε στο Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Re-open MyCrypto on a secure (SSL) connection ",
"ADD_DIGITALBITBOX_0B": "Re-open MyCrypto using [Chrome](https://www.google.com/chrome/browser/desktop/) or [Opera](https://www.opera.com/) ",

View File

@ -81,6 +81,9 @@
"ORDER_TREZOR": "Dont have a TREZOR? Order one now!",
"HOWTO_TREZOR": "How to use TREZOR with MyCrypto",
"X_KEEPKEY": "KeepKey",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connect to Safe-T mini ",
"ORDER_SAFE_T": "Dont have a Safe-T mini? Order one now!",
"UNLOCK_WALLET": "Unlock your",
"X_PARITYSIGNER": "Parity Signer ",
"ADD_PARITY_DESC": "Connect & sign via your Parity Signer mobile app ",
@ -460,6 +463,7 @@
"LEDGER_WRONG_APP": "Wrong application selected on your device",
"LEDGER_LOCKED": "Your Ledger device is locked",
"TREZOR_REFERAL": "Buy a TREZOR",
"SAFE_T_REFERAL": "Buy a Safe-T mini",
"KEEPKEY_REFERRAL": "Buy a Keepkey",
"STEELY_REFERRAL": "Get a Steely",
"ETHERCARD_REFERAL": "Get an ether.card",
@ -643,7 +647,7 @@
"SHAPESHIFT_PROMO_2": "& Tokens",
"COINBASE_PROMO_SMALL": "Its now easier to get more ETH",
"COINBASE_PROMO": "Buy ETH with USD",
"SIMPLEX_PROMO": "Buy ETH with EUR",
"SIMPLEX_PROMO": "Buy ETH with Credit Card",
"TESTNET": "Testnet",
"ENCLAVE_LEDGER_FAIL": "Failed to connect to Ledger",
"ENCLAVE_LEDGER_IN_USE": "Your Ledger is currently in use with another application. Please wait, or close other wallet applications before trying again.",
@ -660,7 +664,11 @@
"NETWORK_2": "network",
"PROVIDED_BY": "provided by",
"YOU_ARE_INTERACTING": "You are interacting with the",
"ANNOUNCEMENT_MESSAGE": "Welcome to the new MyCrypto. We hope you like it! If it's urgent and you need the old site, you can still use [MyCrypto Legacy](https://legacy.mycrypto.com)",
"U2F_NOT_SUPPORTED": "The U2F standard that hardware wallets use does not seem to be supported by your browser. Please try again using Google Chrome."
"USING_PAYMENT_ID": "Using the required Payment ID of:",
"PAYMENT_ID_WARNING": "Don't forget to send your XMR with the payment ID [[?]](https://getmonero.org/resources/moneropedia/paymentid.html) above, or you WILL lose your funds.",
"WHAT_IS_PAYMENT_ID": "what's a payment ID?",
"ANNOUNCEMENT_MESSAGE": "MyCrypto.com no longer allows the use of private keys, mnemonics, or keystore files in the browser. To continue using them, please download the [MyCrypto Desktop App](https://download.mycrypto.com).",
"U2F_NOT_SUPPORTED": "The U2F standard that hardware wallets use does not seem to be supported by your browser. Please try again using Google Chrome.",
"SIMILAR_TRANSACTION_ERROR": "This transaction is very similar to a recent transaction. Please wait a few moments and try again, or click 'Advanced' and manually set the nonce to a new value."
}
}

View File

@ -295,6 +295,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Conectar a TREZOR ",
"ADD_TREZOR_SELECT": "Esto es una semilla TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Conectar a Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Volver a abrir MyCrypto en una conexión segura (SSL) ",
"ADD_DIGITALBITBOX_0B": "Volver a abrir MyCrypto usando [Chrome](https://www.google.com/chrome/browser/desktop/) u [Opera](https://www.opera.com/) ",

View File

@ -296,6 +296,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Connect to TREZOR ",
"ADD_TREZOR_SELECT": "This is a TREZOR seed ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connect to Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Re-open MyCrypto on a secure (SSL) connection ",
"ADD_DIGITALBITBOX_0B": "Re-open MyCrypto using [Chrome](https://www.google.com/chrome/browser/desktop/) or [Opera](https://www.opera.com/) ",

View File

@ -80,6 +80,9 @@
"ADD_TREZOR_SCAN": "Connexion au TREZOR ",
"ORDER_TREZOR": "Pas de TREZOR ? Achetez-en un maintenant !",
"HOWTO_TREZOR": "Comment utiliser TREZOR avec MyCrypto",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connexion au Safe-T mini ",
"ORDER_SAFE_T": "Pas de Safe-T mini ? Achetez-en un maintenant !",
"X_KEEPKEY": "KeepKey",
"UNLOCK_WALLET": "Déverrouillez votre",
"X_PARITYSIGNER": "Parity Signer ",
@ -460,6 +463,7 @@
"LEDGER_WRONG_APP": "Mauvaise application sélectionnée sur votre appareil",
"LEDGER_LOCKED": "Votre appareil Ledger est verrouillé",
"TREZOR_REFERAL": "Acheter un TREZOR",
"SAFE_T_REFERAL": "Acheter un Safe-T mini",
"KEEPKEY_REFERRAL": "Acheter un Keepkey",
"STEELY_REFERRAL": "Acheter un Steely",
"ETHERCARD_REFERAL": "Acheter une ether.card",

View File

@ -136,6 +136,8 @@
"ADD_METAMASK": "Connect to MetaMask ",
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Connect to TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connect to Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Re-open MyCrypto on a secure (SSL) connection ",
"ADD_DIGITALBITBOX_0B": "Re-open MyCrypto using [Chrome](https://www.google.com/chrome/browser/desktop/) or [Opera](https://www.opera.com/) ",

View File

@ -296,6 +296,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Connect to TREZOR ",
"ADD_TREZOR_SELECT": "This is a TREZOR seed ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Connect to Safe-T mini ",
"ADD_METAMASK": "Connect to MetaMask ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Re-open MyCrypto on a secure (SSL) connection ",

View File

@ -160,6 +160,8 @@
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Hubungkan ke TREZOR ",
"ADD_TREZOR_SELECT": "Ini adalah TREZOR seed ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Hubungkan ke Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Buka kembali MyCrypto melalui koneksi (SSL) yang aman ",
"ADD_DIGITALBITBOX_0B": "Buka kembali MyCrypto menggunakan [Chrome](https://www.google.com/chrome/browser/desktop/) atau [Opera](https://www.opera.com/) ",

View File

@ -244,6 +244,8 @@
"ADD_METAMASK": "Collegati a MetaMask ",
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Collegati al TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Collegati al Safe-T mini ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Riapri MyCrypto su una connessione sicura (SSL) ",
"ADD_DIGITALBITBOX_0B": "Riapri MyCrypto utilizzando [Chrome](https://www.google.com/chrome/browser/desktop/) o [Opera](https://www.opera.com/) ",

View File

@ -61,6 +61,8 @@
"ADD_METAMASK": "MetaMaskに接続 ",
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "TREZORに接続する ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Safe-T miniに接続する ",
"X_PARITYSIGNER": "Parity Signer ",
"ADD_PARITY_DESC": "Parity Signerモバイルアプリ経由で接続て署名 ",
"ADD_PARITY_1": "処理が取り消されました ",
@ -394,6 +396,7 @@
"LEDGER_WRONG_APP": "デバイス上で誤ったアプリが選択されています。",
"LEDGER_LOCKED": "Ledgerデバイスがロックされています",
"TREZOR_REFERAL": "TREZORを購入",
"SAFE_T_REFERAL": "Safe-T miniを購入",
"KEEPKEY_REFERRAL": "Keepkeyを購入",
"STEELY_REFERRAL": "Steelyを購入",
"ETHERCARD_REFERAL": "ether.cardを購入",

View File

@ -78,6 +78,8 @@
"X_HARDWARE_WALLET": "하드웨어 지갑",
"X_HARDWARE_WALLET_2": "하드웨어 지갑 ",
"ADD_TREZOR_SCAN": "TREZOR에 연결하기 ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Safe-T mini 에 연결하기 ",
"X_KEEPKEY": "KeepKey 지갑",
"X_PARITYSIGNER": "Parity Signer ",
"ADD_PARITY_DESC": "Parity Signer 모바일 앱을 통해 연결 및 서명 ",

View File

@ -134,6 +134,8 @@
"ADD_LEDGER_SCAN": "Verbind met Ledger Wallet ",
"X_TREZOR": "TREZOR ",
"ADD_TREZOR_SCAN": "Verbind met TREZOR ",
"X_SAFE_T": "Safe-T mini ",
"ADD_SAFE_T_SCAN": "Verbind met Safe-T mini ",
"ADD_METAMASK": "Verbind met MetaMask ",
"X_DIGITALBITBOX": "Digital Bitbox ",
"ADD_DIGITALBITBOX_0A": "Her-open MyCrypto met een veilige (SSL) verbinding ",

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