diff --git a/common/api/shapeshift.ts b/common/api/shapeshift.ts index 3cd255fa..e923da27 100644 --- a/common/api/shapeshift.ts +++ b/common/api/shapeshift.ts @@ -26,7 +26,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; diff --git a/common/components/ui/Warning.scss b/common/components/ui/Warning.scss new file mode 100644 index 00000000..83f6a420 --- /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 00000000..c0c0c10c --- /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 602e8cb5..8f0a4ce5 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 cdb9ce21..1ff1e6e4 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 f7a3d575..aa9820fe 100644 --- a/common/config/data.tsx +++ b/common/config/data.tsx @@ -38,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/containers/OnboardModal/components/WelcomeSlide.scss b/common/containers/OnboardModal/components/WelcomeSlide.scss deleted file mode 100644 index 724c01cd..00000000 --- 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 31df841b..cd2afd33 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/Tabs/Swap/components/CurrentRates.scss b/common/containers/Tabs/Swap/components/CurrentRates.scss index 434ca4eb..fd54b24e 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 9dafe63c..9877c928 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 eaa3d490..fe4fc70c 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 5d173608..fa514a42 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 a9ad299a..fbf302e5 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 5f8b229e..a378c382 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) { @@ -151,8 +153,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 +168,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 4ec51e31..4fe0bf9d 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 41981e4f..8a0fbf89 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 5ff09533..e61c14d9 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/spec/pages/__snapshots__/Swap.spec.tsx.snap b/spec/pages/__snapshots__/Swap.spec.tsx.snap index 5e224002..700e713f 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} /> `;