From dc90565c88bb57aa0f2aed96cf68c78c3a0ee903 Mon Sep 17 00:00:00 2001 From: Connor Bryan Date: Mon, 25 Jun 2018 17:46:04 -0500 Subject: [PATCH] Add monero swap support --- common/api/shapeshift.ts | 2 +- common/components/ui/Warning.scss | 31 +++++++++++++++++ common/components/ui/Warning.tsx | 18 ++++++++++ common/components/ui/index.ts | 1 + common/config/bity.ts | 5 +-- .../OnboardModal/components/WelcomeSlide.scss | 27 --------------- .../OnboardModal/components/WelcomeSlide.tsx | 23 ++++--------- .../Tabs/Swap/components/PartThree.tsx | 8 ++++- .../Tabs/Swap/components/PaymentInfo.scss | 17 ++++++++-- .../Tabs/Swap/components/PaymentInfo.tsx | 34 ++++++++++++++++--- .../Tabs/Swap/components/ReceivingAddress.tsx | 21 ++++++------ common/containers/Tabs/Swap/index.tsx | 10 +++++- common/features/swap/reducer.spec.ts | 7 ++-- common/features/swap/reducer.ts | 11 ++++-- common/features/swap/types.ts | 13 +++++++ common/libs/validators.ts | 6 ++++ common/translations/lang/en.json | 5 ++- spec/pages/__snapshots__/Swap.spec.tsx.snap | 2 ++ 18 files changed, 170 insertions(+), 71 deletions(-) create mode 100644 common/components/ui/Warning.scss create mode 100644 common/components/ui/Warning.tsx delete mode 100644 common/containers/OnboardModal/components/WelcomeSlide.scss 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..a63795c0 --- /dev/null +++ b/common/components/ui/Warning.scss @@ -0,0 +1,31 @@ +@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; + + &-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..28bf27e2 --- /dev/null +++ b/common/components/ui/Warning.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import './Warning.scss'; + +interface WarningProps { + children: any; +} + +export default function Warning(props: WarningProps) { + return ( +
+
+ +
+
{props.children}
+
+ ); +} 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/containers/OnboardModal/components/WelcomeSlide.scss b/common/containers/OnboardModal/components/WelcomeSlide.scss deleted file mode 100644 index cf0fd6df..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 $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: $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/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 { public render() { - const { origin } = this.props; + const { origin, paymentAddress, paymentId, xmrPaymentAddress } = this.props; + const isXMRSwap = origin.label === 'XMR'; + const actualPaymentAddress = isXMRSwap ? xmrPaymentAddress : paymentAddress; + return (

    @@ -22,11 +27,32 @@ export default class PaymentInfo extends PureComponent { })}

    + {isXMRSwap && ( +
    +

    {translate('PAYMENT_ID')}

    + +

    {translate('PAYMENT_ID_WARNING')}

    + + {translate('WHAT_IS_PAYMENT_ID')} + +
    + +
    + )}
    ); } diff --git a/common/containers/Tabs/Swap/components/ReceivingAddress.tsx b/common/containers/Tabs/Swap/components/ReceivingAddress.tsx index 5d173608..7c4b05b0 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,14 @@ 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); return (
    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 09b2b0f3..3c0cf3ba 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 42d23879..5b24d486 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -655,6 +655,9 @@ "NETWORK": "Network", "NETWORK_2": "network", "PROVIDED_BY": "provided by", - "YOU_ARE_INTERACTING": "You are interacting with the" + "YOU_ARE_INTERACTING": "You are interacting with the", + "PAYMENT_ID": "Payment ID:", + "PAYMENT_ID_WARNING": "Don't forget to send your XMR with this payment ID, or you will lose your money!", + "WHAT_IS_PAYMENT_ID": "what's a payment ID?" } } 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} /> `;