diff --git a/common/api/shapeshift.spec.ts b/common/api/shapeshift.spec.ts new file mode 100644 index 00000000000..8df9ceb4d1f --- /dev/null +++ b/common/api/shapeshift.spec.ts @@ -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(); + }); +}); diff --git a/common/api/shapeshift.ts b/common/api/shapeshift.ts index 3cd255fa959..1f1d040cd09 100644 --- a/common/api/shapeshift.ts +++ b/common/api/shapeshift.ts @@ -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() { diff --git a/common/components/AppAlphaNotice/AlphaNotice.scss b/common/components/AppAlphaNotice/AlphaNotice.scss deleted file mode 100644 index d4b3c5be1e0..00000000000 --- a/common/components/AppAlphaNotice/AlphaNotice.scss +++ /dev/null @@ -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; - } - } -} diff --git a/common/components/AppAlphaNotice/AlphaNotice.tsx b/common/components/AppAlphaNotice/AlphaNotice.tsx deleted file mode 100644 index 9dddd3a1505..00000000000 --- a/common/components/AppAlphaNotice/AlphaNotice.tsx +++ /dev/null @@ -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 ( -
-
-

Welcome to the MyCrypto Desktop App Alpha

-

- 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. -

-

- Because this is for testing purposes only,{' '} - this build of the app will only be accessible until {expDate}. You’ll - then be required to update the application to continue using it. -

-

- Feedback and bug reports are greatly appreciated. You can file issues on our{' '} - - GitHub repository - {' '} - or join our Discord server to discuss the - app. -

-

- - For critical reports & vulnerabilities, please use{' '} - HackerOne. - -

- - -
-
- ); - } - - private doContinue = () => { - hasAcknowledged = true; - this.setState({ isFading: true }); - setTimeout(() => { - this.setState({ isClosed: true }); - }, 1000); - }; -} diff --git a/common/components/AppAlphaNotice/AppExpired.scss b/common/components/AppAlphaNotice/AppExpired.scss deleted file mode 100644 index 867e360acdc..00000000000 --- a/common/components/AppAlphaNotice/AppExpired.scss +++ /dev/null @@ -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; - } - } - } -} diff --git a/common/components/AppAlphaNotice/AppExpired.tsx b/common/components/AppAlphaNotice/AppExpired.tsx deleted file mode 100644 index a36a79c48cb..00000000000 --- a/common/components/AppAlphaNotice/AppExpired.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { NewTabLink } from 'components/ui'; -import './AppExpired.scss'; - -const AppExpired: React.SFC<{}> = () => ( -
-
-

Your Alpha Build Has Expired

-

- 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! -

- - - Download a New Build - -
-
-); - -export default AppExpired; diff --git a/common/components/AppAlphaNotice/index.tsx b/common/components/AppAlphaNotice/index.tsx deleted file mode 100644 index 8a66179ec0b..00000000000 --- a/common/components/AppAlphaNotice/index.tsx +++ /dev/null @@ -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 ; - } else { - return ; - } -}; - -export default AppAlphaNotice; diff --git a/common/components/ElectronNav/ElectronNav.tsx b/common/components/ElectronNav/ElectronNav.tsx index 7f8a0ba775d..b70172f38a5 100644 --- a/common/components/ElectronNav/ElectronNav.tsx +++ b/common/components/ElectronNav/ElectronNav.tsx @@ -44,7 +44,6 @@ class ElectronNav extends React.Component { >
-
Alpha Release
    diff --git a/common/components/TXMetaDataPanel/components/SimpleGas.tsx b/common/components/TXMetaDataPanel/components/SimpleGas.tsx index 02b407ac107..79fa10bbca7 100644 --- a/common/components/TXMetaDataPanel/components/SimpleGas.tsx +++ b/common/components/TXMetaDataPanel/components/SimpleGas.tsx @@ -76,6 +76,16 @@ class SimpleGas extends React.Component { 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 (
    @@ -103,7 +113,7 @@ class SimpleGas extends React.Component { 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} /> diff --git a/common/components/index.ts b/common/components/index.ts index 013e66452bd..688f845f1fb 100644 --- a/common/components/index.ts +++ b/common/components/index.ts @@ -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'; diff --git a/common/components/ui/Dropdown.tsx b/common/components/ui/Dropdown.tsx index b68774de4aa..825116caec6 100644 --- a/common/components/ui/Dropdown.tsx +++ b/common/components/ui/Dropdown.tsx @@ -9,7 +9,7 @@ interface Props extends ReactSelectProps { export default class Dropdown extends React.Component { public state = { - selectedOption: { value: '', label: '' }, + selectedOption: { value: undefined, label: '' }, hasBlurred: false }; @@ -43,13 +43,13 @@ export default class Dropdown extends React.Component { }); } }} + {...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) { diff --git a/common/components/ui/SwapDropdown.tsx b/common/components/ui/SwapDropdown.tsx index b044ac658ec..94aa1b8ee69 100644 --- a/common/components/ui/SwapDropdown.tsx +++ b/common/components/ui/SwapDropdown.tsx @@ -87,7 +87,7 @@ class SwapDropdown extends PureComponent { 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 { (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 }); } } diff --git a/common/components/ui/Warning.scss b/common/components/ui/Warning.scss new file mode 100644 index 00000000000..83f6a420c90 --- /dev/null +++ b/common/components/ui/Warning.scss @@ -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; + } +} diff --git a/common/components/ui/Warning.tsx b/common/components/ui/Warning.tsx new file mode 100644 index 00000000000..c0c0c10c8dc --- /dev/null +++ b/common/components/ui/Warning.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import './Warning.scss'; + +interface WarningProps { + highlighted?: boolean; +} + +const Warning: React.SFC = ({ highlighted, children }) => { + const className = `Warning ${highlighted ? 'highlighted' : ''}`; + + return ( +
    +
    + +
    +
    {children}
    +
    + ); +}; + +export default Warning; diff --git a/common/components/ui/index.ts b/common/components/ui/index.ts index 602e8cb50c0..8f0a4ce5dc2 100644 --- a/common/components/ui/index.ts +++ b/common/components/ui/index.ts @@ -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'; diff --git a/common/config/bity.ts b/common/config/bity.ts index cdb9ce2192c..1ff1e6e4740 100644 --- a/common/config/bity.ts +++ b/common/config/bity.ts @@ -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] diff --git a/common/config/data.tsx b/common/config/data.tsx index 01fa50aea43..aa9820feb5b 100644 --- a/common/config/data.tsx +++ b/common/config/data.tsx @@ -12,12 +12,6 @@ export const discordURL = 'https://discord.gg/VSaTXEA'; 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. @@ -44,7 +38,9 @@ export const etherChainExplorerInst = makeExplorer({ export const donationAddressMap = { BTC: '32oirLEzZRhi33RCXDF9WHJjEb8RsrSss3', ETH: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', - REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520' + REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', + XMR: + '4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CW7F7tDFvS511SNfZV7' }; export const gasEstimateCacheTime = 60000; diff --git a/common/config/tokens/eth.json b/common/config/tokens/eth.json index fda4da48003..c486f72a845 100644 --- a/common/config/tokens/eth.json +++ b/common/config/tokens/eth.json @@ -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 } -] \ No newline at end of file +] diff --git a/common/containers/OnboardModal/components/WelcomeSlide.scss b/common/containers/OnboardModal/components/WelcomeSlide.scss deleted file mode 100644 index 724c01cd177..00000000000 --- a/common/containers/OnboardModal/components/WelcomeSlide.scss +++ /dev/null @@ -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); - } - } -} diff --git a/common/containers/OnboardModal/components/WelcomeSlide.tsx b/common/containers/OnboardModal/components/WelcomeSlide.tsx index 31df841b174..cd2afd33792 100644 --- a/common/containers/OnboardModal/components/WelcomeSlide.tsx +++ b/common/containers/OnboardModal/components/WelcomeSlide.tsx @@ -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 = {translate('ONBOARD_WELCOME_CONTENT__3')}; const content = (
    -
    -
    - -
    - - {translate('ONBOARD_WELCOME_CONTENT__1')} - {translate('ONBOARD_WELCOME_CONTENT__2')} - -
    -
    -
    - -
    - {translate('ONBOARD_WELCOME_CONTENT__8')} -
    + + {translate('ONBOARD_WELCOME_CONTENT__1')} + {translate('ONBOARD_WELCOME_CONTENT__2')} + + {translate('ONBOARD_WELCOME_CONTENT__8')}
    {translate('ONBOARD_WELCOME_CONTENT__4')}
    • {translate('ONBOARD_WELCOME_CONTENT__5')}
    • diff --git a/common/containers/TabSection/ElectronTemplate.tsx b/common/containers/TabSection/ElectronTemplate.tsx index a4884999c52..936d12b1f96 100644 --- a/common/containers/TabSection/ElectronTemplate.tsx +++ b/common/containers/TabSection/ElectronTemplate.tsx @@ -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 { {isUnavailableOffline && isOffline ? : children}
    -
    diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx index fd66eea8b52..37bf7169c30 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx @@ -195,6 +195,12 @@ class InteractForm extends Component { abiJson: fullContract.abi || '', contract }); + } else { + this.props.setCurrentTo(''); + this.setState({ + abiJson: '', + contract + }); } }; diff --git a/common/containers/Tabs/Swap/components/CurrentRates.scss b/common/containers/Tabs/Swap/components/CurrentRates.scss index 434ca4ebcd4..fd54b24e66d 100644 --- a/common/containers/Tabs/Swap/components/CurrentRates.scss +++ b/common/containers/Tabs/Swap/components/CurrentRates.scss @@ -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; diff --git a/common/containers/Tabs/Swap/components/PartThree.tsx b/common/containers/Tabs/Swap/components/PartThree.tsx index 9dafe63cfda..9877c928aac 100644 --- a/common/containers/Tabs/Swap/components/PartThree.tsx +++ b/common/containers/Tabs/Swap/components/PartThree.tsx @@ -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 * { + 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; + } + } } diff --git a/common/containers/Tabs/Swap/components/PaymentInfo.tsx b/common/containers/Tabs/Swap/components/PaymentInfo.tsx index eaa3d490be0..fe4fc70c7bb 100644 --- a/common/containers/Tabs/Swap/components/PaymentInfo.tsx +++ b/common/containers/Tabs/Swap/components/PaymentInfo.tsx @@ -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 { public render() { - const { origin } = this.props; + const { origin, paymentAddress, paymentId, xmrPaymentAddress } = this.props; + const isXMRSwap = origin.label === 'XMR'; + const actualPaymentAddress = isXMRSwap ? xmrPaymentAddress : paymentAddress; + return (
    + {isXMRSwap && ( +
    +

    + {translate('USING_PAYMENT_ID')} + +

    +
    + )}

    {translate('SWAP_SEND_TO', { $origin_amount: origin.amount.toString(), @@ -22,11 +50,16 @@ export default class PaymentInfo extends PureComponent { })}

    + {isXMRSwap && ( + +

    {translate('PAYMENT_ID_WARNING')}

    +
    + )}
    ); } diff --git a/common/containers/Tabs/Swap/components/ReceivingAddress.tsx b/common/containers/Tabs/Swap/components/ReceivingAddress.tsx index 5d173608470..fa514a42f54 100644 --- a/common/containers/Tabs/Swap/components/ReceivingAddress.tsx +++ b/common/containers/Tabs/Swap/components/ReceivingAddress.tsx @@ -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 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 (
    @@ -85,11 +94,7 @@ export default class ReceivingAddress extends PureComponent
    diff --git a/common/containers/Tabs/Swap/index.tsx b/common/containers/Tabs/Swap/index.tsx index a9ad299a7f3..fbf302e5726 100644 --- a/common/containers/Tabs/Swap/index.tsx +++ b/common/containers/Tabs/Swap/index.tsx @@ -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 { 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 }); }); diff --git a/common/features/swap/reducer.ts b/common/features/swap/reducer.ts index 5f8b229e0da..e75ef7cc9b0 100644 --- a/common/features/swap/reducer.ts +++ b/common/features/swap/reducer.ts @@ -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 { diff --git a/common/features/swap/types.ts b/common/features/swap/types.ts index 4ec51e319f0..4fe0bf9d9de 100644 --- a/common/features/swap/types.ts +++ b/common/features/swap/types.ts @@ -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 { diff --git a/common/libs/validators.ts b/common/libs/validators.ts index 41981e4fa7d..8a0fbf891ea 100644 --- a/common/libs/validators.ts +++ b/common/libs/validators.ts @@ -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; diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index 5ff0953354f..e61c14d97e3 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -660,6 +660,9 @@ "NETWORK_2": "network", "PROVIDED_BY": "provided by", "YOU_ARE_INTERACTING": "You are interacting with the", + "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": "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." } diff --git a/electron-app/main/window.ts b/electron-app/main/window.ts index 11d0c549a91..bd5f11f9414 100644 --- a/electron-app/main/window.ts +++ b/electron-app/main/window.ts @@ -67,6 +67,10 @@ export default function getWindow() { }); }); + window.webContents.on('will-navigate', (event: any) => { + event.preventDefault(); + }); + if (isDevelopment) { window.webContents.on('did-fail-load', () => { setTimeout(() => { diff --git a/spec/pages/__snapshots__/Swap.spec.tsx.snap b/spec/pages/__snapshots__/Swap.spec.tsx.snap index 5e224002027..700e713f277 100644 --- a/spec/pages/__snapshots__/Swap.spec.tsx.snap +++ b/spec/pages/__snapshots__/Swap.spec.tsx.snap @@ -76,6 +76,7 @@ exports[`render snapshot 1`] = ` } outputTx={null} paymentAddress={null} + paymentId={null} provider="shapeshift" restartSwap={[Function]} secondsRemaining={null} @@ -100,5 +101,6 @@ exports[`render snapshot 1`] = ` stopPollBityOrderStatus={[Function]} stopPollShapeshiftOrderStatus={[Function]} swapProvider={[Function]} + xmrPaymentAddress={null} /> `; diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 00000000000..a0011ec06f1 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Sitemap: https://mycrypto.com/sitemap.xml diff --git a/static/sitemap.xml b/static/sitemap.xml new file mode 100644 index 00000000000..c1a9b5b6d54 --- /dev/null +++ b/static/sitemap.xml @@ -0,0 +1,13 @@ + + +https://mycrypto.com0.5 +https://mycrypto.com/0.5 +https://mycrypto.com/account0.5 +https://mycrypto.com/generate0.5 +https://mycrypto.com/swap0.5 +https://mycrypto.com/contracts0.5 +https://mycrypto.com/ens0.5 +https://mycrypto.com/sign-and-verify-message0.5 +https://mycrypto.com/tx-status0.5 +https://mycrypto.com/pushTx0.5 + \ No newline at end of file