@@ -24,7 +28,7 @@ export default class SwapInfoHeaderTitle extends Component
diff --git a/common/containers/Tabs/Swap/components/SwapProgress.tsx b/common/containers/Tabs/Swap/components/SwapProgress.tsx
index d5d11185..ce390c59 100644
--- a/common/containers/Tabs/Swap/components/SwapProgress.tsx
+++ b/common/containers/Tabs/Swap/components/SwapProgress.tsx
@@ -9,7 +9,9 @@ export interface Props {
originId: string;
destinationAddress: string;
outputTx: string;
- orderStatus: string | null;
+ provider: string;
+ bityOrderStatus: string | null;
+ shapeshiftOrderStatus: string | null;
// actions
showNotification: TShowNotification;
}
@@ -28,9 +30,17 @@ export default class SwapProgress extends Component {
public showSwapNotification = () => {
const { hasShownViewTx } = this.state;
- const { destinationId, outputTx, showNotification, orderStatus } = this.props;
+ const {
+ destinationId,
+ outputTx,
+ showNotification,
+ provider,
+ bityOrderStatus,
+ shapeshiftOrderStatus
+ } = this.props;
+ const isShapeshift = provider === 'shapeshift';
- if (orderStatus === 'FILL') {
+ if (isShapeshift ? shapeshiftOrderStatus === 'complete' : bityOrderStatus === 'FILL') {
if (!hasShownViewTx) {
let linkElement: React.ReactElement;
let link;
@@ -40,7 +50,7 @@ export default class SwapProgress extends Component {
link = bityConfig.ETHTxExplorer(outputTx);
linkElement = (
- ${notificationMessage}
+ {notificationMessage}
);
// BTC uses a different explorer
@@ -48,7 +58,7 @@ export default class SwapProgress extends Component {
link = bityConfig.BTCTxExplorer(outputTx);
linkElement = (
- ${notificationMessage}
+ {notificationMessage}
);
}
@@ -60,11 +70,12 @@ export default class SwapProgress extends Component {
};
public computedClass = (step: number) => {
- const { orderStatus } = this.props;
+ const { bityOrderStatus, shapeshiftOrderStatus } = this.props;
let cssClass = 'SwapProgress-item';
-
+ const orderStatus = bityOrderStatus || shapeshiftOrderStatus;
switch (orderStatus) {
+ case 'no_deposits':
case 'OPEN':
if (step < 2) {
return cssClass + ' is-complete';
@@ -73,6 +84,7 @@ export default class SwapProgress extends Component {
} else {
return cssClass;
}
+ case 'received':
case 'RCVE':
if (step < 4) {
return cssClass + ' is-complete';
@@ -81,9 +93,11 @@ export default class SwapProgress extends Component {
} else {
return cssClass;
}
+ case 'complete':
case 'FILL':
cssClass += ' is-complete';
return cssClass;
+ case 'failed':
case 'CANC':
return cssClass;
default:
diff --git a/common/containers/Tabs/Swap/index.tsx b/common/containers/Tabs/Swap/index.tsx
index d90701c4..7803e248 100644
--- a/common/containers/Tabs/Swap/index.tsx
+++ b/common/containers/Tabs/Swap/index.tsx
@@ -2,91 +2,128 @@ import { showNotification as dShowNotification, TShowNotification } from 'action
import {
initSwap as dInitSwap,
bityOrderCreateRequestedSwap as dBityOrderCreateRequestedSwap,
+ shapeshiftOrderCreateRequestedSwap as dShapeshiftOrderCreateRequestedSwap,
changeStepSwap as dChangeStepSwap,
destinationAddressSwap as dDestinationAddressSwap,
loadBityRatesRequestedSwap as dLoadBityRatesRequestedSwap,
+ loadShapeshiftRatesRequestedSwap as dLoadShapeshiftRatesRequestedSwap,
restartSwap as dRestartSwap,
startOrderTimerSwap as dStartOrderTimerSwap,
startPollBityOrderStatus as dStartPollBityOrderStatus,
+ startPollShapeshiftOrderStatus as dStartPollShapeshiftOrderStatus,
stopLoadBityRatesSwap as dStopLoadBityRatesSwap,
+ stopLoadShapeshiftRatesSwap as dStopLoadShapeshiftRatesSwap,
stopOrderTimerSwap as dStopOrderTimerSwap,
stopPollBityOrderStatus as dStopPollBityOrderStatus,
+ stopPollShapeshiftOrderStatus as dStopPollShapeshiftOrderStatus,
+ changeSwapProvider as dChangeSwapProvider,
TInitSwap,
TBityOrderCreateRequestedSwap,
TChangeStepSwap,
TDestinationAddressSwap,
TLoadBityRatesRequestedSwap,
+ TShapeshiftOrderCreateRequestedSwap,
+ TLoadShapeshiftRequestedSwap,
TRestartSwap,
TStartOrderTimerSwap,
TStartPollBityOrderStatus,
+ TStartPollShapeshiftOrderStatus,
TStopLoadBityRatesSwap,
TStopOrderTimerSwap,
- TStopPollBityOrderStatus
+ TStopPollBityOrderStatus,
+ TStopPollShapeshiftOrderStatus,
+ TChangeSwapProvider,
+ TStopLoadShapeshiftRatesSwap,
+ ProviderName
} from 'actions/swap';
-import { SwapInput, NormalizedOptions, NormalizedBityRates } from 'reducers/swap/types';
+import {
+ SwapInput,
+ NormalizedOptions,
+ NormalizedBityRates,
+ NormalizedShapeshiftRates
+} from 'reducers/swap/types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import CurrencySwap from './components/CurrencySwap';
import CurrentRates from './components/CurrentRates';
import PartThree from './components/PartThree';
+import SupportFooter from './components/SupportFooter';
import ReceivingAddress from './components/ReceivingAddress';
import SwapInfoHeader from './components/SwapInfoHeader';
+import ShapeshiftBanner from './components/ShapeshiftBanner';
import TabSection from 'containers/TabSection';
+import { merge } from 'lodash';
interface ReduxStateProps {
step: number;
origin: SwapInput;
destination: SwapInput;
bityRates: NormalizedBityRates;
+ shapeshiftRates: NormalizedShapeshiftRates;
options: NormalizedOptions;
+ provider: ProviderName;
bityOrder: any;
+ shapeshiftOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
secondsRemaining: number | null;
outputTx: string | null;
isPostingOrder: boolean;
- orderStatus: string | null;
+ bityOrderStatus: string | null;
+ shapeshiftOrderStatus: string | null;
paymentAddress: string | null;
}
interface ReduxActionProps {
changeStepSwap: TChangeStepSwap;
loadBityRatesRequestedSwap: TLoadBityRatesRequestedSwap;
+ loadShapeshiftRatesRequestedSwap: TLoadShapeshiftRequestedSwap;
destinationAddressSwap: TDestinationAddressSwap;
restartSwap: TRestartSwap;
stopLoadBityRatesSwap: TStopLoadBityRatesSwap;
+ stopLoadShapeshiftRatesSwap: TStopLoadShapeshiftRatesSwap;
+ shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap;
+ startPollShapeshiftOrderStatus: TStartPollShapeshiftOrderStatus;
startPollBityOrderStatus: TStartPollBityOrderStatus;
+ startOrderTimerSwap: TStartOrderTimerSwap;
stopOrderTimerSwap: TStopOrderTimerSwap;
stopPollBityOrderStatus: TStopPollBityOrderStatus;
+ stopPollShapeshiftOrderStatus: TStopPollShapeshiftOrderStatus;
showNotification: TShowNotification;
- startOrderTimerSwap: TStartOrderTimerSwap;
initSwap: TInitSwap;
+ swapProvider: TChangeSwapProvider;
}
class Swap extends Component {
public componentDidMount() {
this.props.loadBityRatesRequestedSwap();
+ this.props.loadShapeshiftRatesRequestedSwap();
}
public componentWillUnmount() {
this.props.stopLoadBityRatesSwap();
+ this.props.stopLoadShapeshiftRatesSwap();
}
public render() {
const {
// STATE
bityRates,
+ shapeshiftRates,
+ provider,
options,
origin,
destination,
destinationAddress,
step,
bityOrder,
+ shapeshiftOrder,
secondsRemaining,
paymentAddress,
- orderStatus,
+ bityOrderStatus,
+ shapeshiftOrderStatus,
isPostingOrder,
outputTx,
// ACTIONS
@@ -96,24 +133,31 @@ class Swap extends Component {
changeStepSwap,
destinationAddressSwap,
bityOrderCreateRequestedSwap,
+ shapeshiftOrderCreateRequestedSwap,
showNotification,
startOrderTimerSwap,
startPollBityOrderStatus,
+ stopPollShapeshiftOrderStatus,
+ startPollShapeshiftOrderStatus,
stopOrderTimerSwap,
- stopPollBityOrderStatus
+ stopPollBityOrderStatus,
+ swapProvider
} = this.props;
- const { reference } = bityOrder;
+ const reference = provider === 'shapeshift' ? shapeshiftOrder.orderId : bityOrder.reference;
const ReceivingAddressProps = {
isPostingOrder,
origin,
destinationId: destination.id,
+ destinationKind: destination.amount as number,
destinationAddressSwap,
destinationAddress,
stopLoadBityRatesSwap,
changeStepSwap,
- bityOrderCreateRequestedSwap
+ provider,
+ bityOrderCreateRequestedSwap,
+ shapeshiftOrderCreateRequestedSwap
};
const SwapInfoHeaderProps = {
@@ -122,19 +166,29 @@ class Swap extends Component {
reference,
secondsRemaining,
restartSwap,
- orderStatus
+ bityOrderStatus,
+ shapeshiftOrderStatus,
+ provider
};
const CurrencySwapProps = {
showNotification,
bityRates,
+ shapeshiftRates,
+ provider,
options,
initSwap,
+ swapProvider,
changeStepSwap
};
+ const paymentInfo =
+ provider === 'shapeshift'
+ ? merge(origin, { amount: shapeshiftOrder.depositAmount })
+ : merge(origin, { amount: bityOrder.amount });
+
const PaymentInfoProps = {
- origin,
+ origin: paymentInfo,
paymentAddress
};
@@ -142,29 +196,44 @@ class Swap extends Component {
...SwapInfoHeaderProps,
...PaymentInfoProps,
reference,
+ provider,
startOrderTimerSwap,
- startPollBityOrderStatus,
stopOrderTimerSwap,
+ startPollBityOrderStatus,
+ startPollShapeshiftOrderStatus,
stopPollBityOrderStatus,
+ stopPollShapeshiftOrderStatus,
showNotification,
destinationAddress,
outputTx
};
- const CurrentRatesProps = bityRates.byId ? { ...bityRates.byId } : {};
+ const SupportProps = {
+ origin,
+ destination,
+ destinationAddress,
+ paymentAddress,
+ reference,
+ provider,
+ shapeshiftRates,
+ bityRates
+ };
+
+ const CurrentRatesProps = { provider, bityRates, shapeshiftRates };
return (
{step === 1 && }
+ {step === 1 && }
{(step === 2 || step === 3) && }
-
{step === 1 && }
{step === 2 && }
{step === 3 && }
+
);
}
@@ -176,14 +245,18 @@ function mapStateToProps(state: AppState) {
origin: state.swap.origin,
destination: state.swap.destination,
bityRates: state.swap.bityRates,
+ shapeshiftRates: state.swap.shapeshiftRates,
+ provider: state.swap.provider,
options: state.swap.options,
bityOrder: state.swap.bityOrder,
+ shapeshiftOrder: state.swap.shapeshiftOrder,
destinationAddress: state.swap.destinationAddress,
isFetchingRates: state.swap.isFetchingRates,
secondsRemaining: state.swap.secondsRemaining,
outputTx: state.swap.outputTx,
isPostingOrder: state.swap.isPostingOrder,
- orderStatus: state.swap.orderStatus,
+ bityOrderStatus: state.swap.bityOrderStatus,
+ shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
paymentAddress: state.swap.paymentAddress
};
}
@@ -192,13 +265,19 @@ export default connect(mapStateToProps, {
changeStepSwap: dChangeStepSwap,
initSwap: dInitSwap,
bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
+ shapeshiftOrderCreateRequestedSwap: dShapeshiftOrderCreateRequestedSwap,
loadBityRatesRequestedSwap: dLoadBityRatesRequestedSwap,
+ loadShapeshiftRatesRequestedSwap: dLoadShapeshiftRatesRequestedSwap,
destinationAddressSwap: dDestinationAddressSwap,
restartSwap: dRestartSwap,
startOrderTimerSwap: dStartOrderTimerSwap,
startPollBityOrderStatus: dStartPollBityOrderStatus,
+ startPollShapeshiftOrderStatus: dStartPollShapeshiftOrderStatus,
stopLoadBityRatesSwap: dStopLoadBityRatesSwap,
+ stopLoadShapeshiftRatesSwap: dStopLoadShapeshiftRatesSwap,
stopOrderTimerSwap: dStopOrderTimerSwap,
stopPollBityOrderStatus: dStopPollBityOrderStatus,
- showNotification: dShowNotification
+ stopPollShapeshiftOrderStatus: dStopPollShapeshiftOrderStatus,
+ showNotification: dShowNotification,
+ swapProvider: dChangeSwapProvider
})(Swap);
diff --git a/common/libs/transaction/utils/index.ts b/common/libs/transaction/utils/index.ts
index 5176d0f1..a92e5f36 100644
--- a/common/libs/transaction/utils/index.ts
+++ b/common/libs/transaction/utils/index.ts
@@ -2,15 +2,7 @@ import { Wei } from 'libs/units';
import * as eth from './ether';
import { IFullWallet } from 'libs/wallet';
import { ITransaction } from '../typings';
-export {
- enoughBalanceViaTx,
- validateTx,
- validGasLimit,
- makeTransaction,
- getTransactionFields,
- computeIndexingHash
-} from './ether';
-export * from './token';
+
export const signTransaction = async (
t: ITransaction,
w: IFullWallet,
@@ -21,3 +13,13 @@ export const signTransaction = async (
const signedT = await eth.signTx(t, w);
return signedT;
};
+
+export {
+ enoughBalanceViaTx,
+ validateTx,
+ validGasLimit,
+ makeTransaction,
+ getTransactionFields,
+ computeIndexingHash
+} from './ether';
+export * from './token';
diff --git a/common/libs/units.ts b/common/libs/units.ts
index b3728160..b2afda8e 100644
--- a/common/libs/units.ts
+++ b/common/libs/units.ts
@@ -103,7 +103,7 @@ const fromTokenBase = (value: TokenValue, decimal: number) =>
const toTokenBase = (value: string, decimal: number) =>
TokenValue(convertedToBaseUnit(value, decimal));
-const isEtherUnit = (unit: string) => unit === 'ether';
+const isEtherUnit = (unit: string) => unit === 'ether' || unit === 'ETH';
const convertTokenBase = (value: TokenValue, oldDecimal: number, newDecimal: number) => {
if (oldDecimal === newDecimal) {
diff --git a/common/reducers/swap/index.ts b/common/reducers/swap/index.ts
index e4f4ca59..2ea56dc3 100644
--- a/common/reducers/swap/index.ts
+++ b/common/reducers/swap/index.ts
@@ -10,17 +10,23 @@ export interface State {
destination: stateTypes.SwapInput;
options: stateTypes.NormalizedOptions;
bityRates: stateTypes.NormalizedBityRates;
+ // Change this
+ shapeshiftRates: stateTypes.NormalizedBityRates;
+ provider: string;
bityOrder: any;
+ shapeshiftOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
secondsRemaining: number | null;
outputTx: string | null;
isPostingOrder: boolean;
- orderStatus: string | null;
+ bityOrderStatus: string | null;
+ shapeshiftOrderStatus: string | null;
orderTimestampCreatedISOString: string | null;
paymentAddress: string | null;
validFor: number | null;
orderId: string | null;
+ showLiteSend: boolean;
}
export const INITIAL_STATE: State = {
@@ -35,17 +41,25 @@ export const INITIAL_STATE: State = {
byId: {},
allIds: []
},
+ shapeshiftRates: {
+ byId: {},
+ allIds: []
+ },
+ provider: 'bity',
destinationAddress: '',
bityOrder: {},
+ shapeshiftOrder: {},
isFetchingRates: null,
secondsRemaining: null,
outputTx: null,
isPostingOrder: false,
- orderStatus: null,
+ bityOrderStatus: null,
+ shapeshiftOrderStatus: null,
orderTimestampCreatedISOString: null,
paymentAddress: null,
validFor: null,
- orderId: null
+ orderId: null,
+ showLiteSend: false
};
export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapAction) {
@@ -55,12 +69,41 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
return {
...state,
bityRates: {
- byId: normalize(payload, [schema.bityRate]).entities.bityRates,
- allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.bityRates)
+ byId: normalize(payload, [schema.providerRate]).entities.providerRates,
+ allIds: schema.allIds(normalize(payload, [schema.providerRate]).entities.providerRates)
},
options: {
- byId: normalize(payload, [schema.bityRate]).entities.options,
- allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.options)
+ byId: Object.assign(
+ {},
+ normalize(payload, [schema.providerRate]).entities.options,
+ state.options.byId
+ ),
+ allIds: [
+ ...schema.allIds(normalize(payload, [schema.providerRate]).entities.options),
+ ...state.options.allIds
+ ]
+ },
+ isFetchingRates: false
+ };
+ case TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED:
+ return {
+ ...state,
+ shapeshiftRates: {
+ byId: normalize(action.payload, [schema.providerRate]).entities.providerRates,
+ allIds: schema.allIds(
+ normalize(action.payload, [schema.providerRate]).entities.providerRates
+ )
+ },
+ options: {
+ byId: Object.assign(
+ {},
+ normalize(action.payload, [schema.providerRate]).entities.options,
+ state.options.byId
+ ),
+ allIds: [
+ ...schema.allIds(normalize(action.payload, [schema.providerRate]).entities.options),
+ ...state.options.allIds
+ ]
},
isFetchingRates: false
};
@@ -85,19 +128,30 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
case TypeKeys.SWAP_RESTART:
return {
...INITIAL_STATE,
- bityRates: state.bityRates
+ options: state.options,
+ bityRates: state.bityRates,
+ shapeshiftRates: state.shapeshiftRates
};
- case TypeKeys.SWAP_ORDER_CREATE_REQUESTED:
+ case TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED:
return {
...state,
isPostingOrder: true
};
- case TypeKeys.SWAP_ORDER_CREATE_FAILED:
+ case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED:
+ return {
+ ...state,
+ isPostingOrder: true
+ };
+ case TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED:
+ return {
+ ...state,
+ isPostingOrder: false
+ };
+ case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED:
return {
...state,
isPostingOrder: false
};
- // TODO - fix bad naming
case TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED:
return {
...state,
@@ -111,18 +165,43 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
validFor: action.payload.validFor, // to build from local storage
orderTimestampCreatedISOString: action.payload.timestamp_created,
paymentAddress: action.payload.payment_address,
- orderStatus: action.payload.status,
+ bityOrderStatus: action.payload.status,
orderId: action.payload.id
};
+ case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED:
+ const currDate = Date.now();
+
+ const secondsRemaining = Math.floor((+new Date(action.payload.expiration) - currDate) / 1000);
+ return {
+ ...state,
+ shapeshiftOrder: {
+ ...action.payload
+ },
+ isPostingOrder: false,
+ originAmount: parseFloat(action.payload.depositAmount),
+ destinationAmount: parseFloat(action.payload.withdrawalAmount),
+ secondsRemaining,
+ validFor: secondsRemaining,
+ orderTimestampCreatedISOString: new Date(currDate).toISOString(),
+ paymentAddress: action.payload.deposit,
+ shapeshiftOrderStatus: 'no_deposits',
+ orderId: action.payload.orderId
+ };
case TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED:
return {
...state,
outputTx: action.payload.output.reference,
- orderStatus:
+ bityOrderStatus:
action.payload.output.status === 'FILL'
? action.payload.output.status
: action.payload.input.status
};
+ case TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED:
+ return {
+ ...state,
+ outputTx: action.payload && action.payload.transaction ? action.payload.transaction : null,
+ shapeshiftOrderStatus: action.payload.status
+ };
case TypeKeys.SWAP_ORDER_TIME:
return {
...state,
@@ -134,13 +213,31 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
...state,
isFetchingRates: true
};
-
+ case TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED:
+ return {
+ ...state,
+ isFetchingRates: true
+ };
case TypeKeys.SWAP_STOP_LOAD_BITY_RATES:
return {
...state,
isFetchingRates: false
};
-
+ case TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES:
+ return {
+ ...state,
+ isFetchingRates: false
+ };
+ case TypeKeys.SWAP_CHANGE_PROVIDER:
+ return {
+ ...state,
+ provider: action.payload
+ };
+ case TypeKeys.SWAP_SHOW_LITE_SEND:
+ return {
+ ...state,
+ showLiteSend: action.payload
+ };
default:
return state;
}
diff --git a/common/reducers/swap/schema.ts b/common/reducers/swap/schema.ts
index cf9a0801..f6e9827c 100644
--- a/common/reducers/swap/schema.ts
+++ b/common/reducers/swap/schema.ts
@@ -3,7 +3,8 @@ import { schema } from 'normalizr';
export const allIds = (byIds: { [name: string]: {} }) => {
return Object.keys(byIds);
};
+
export const option = new schema.Entity('options');
-export const bityRate = new schema.Entity('bityRates', {
+export const providerRate = new schema.Entity('providerRates', {
options: [option]
});
diff --git a/common/reducers/swap/types.ts b/common/reducers/swap/types.ts
index 8ccb6137..363988bf 100644
--- a/common/reducers/swap/types.ts
+++ b/common/reducers/swap/types.ts
@@ -3,10 +3,21 @@ import { WhitelistedCoins } from 'config/bity';
export interface SwapInput {
id: WhitelistedCoins;
- amount: number;
+ amount: number | string;
}
-export interface NormalizedBityRate {
+export interface NormalizedRate {
+ id: number;
+ options: WhitelistedCoins[];
+ rate: number;
+}
+
+export interface NormalizedRates {
+ byId: { [id: string]: NormalizedRate };
+ allIds: string[];
+}
+
+export interface NormalizedBityRate extends NormalizedRate {
id: number;
options: WhitelistedCoins[];
rate: number;
@@ -17,6 +28,19 @@ export interface NormalizedBityRates {
allIds: string[];
}
+export interface NormalizedShapeshiftRate extends NormalizedRate {
+ id: number;
+ options: WhitelistedCoins[];
+ rate: number;
+ limit: number;
+ min: number;
+}
+
+export interface NormalizedShapeshiftRates {
+ byId: { [id: string]: NormalizedShapeshiftRate };
+ allIds: string[];
+}
+
export interface NormalizedOptions {
byId: { [id: string]: Option };
allIds: string[];
diff --git a/common/reducers/wallet.ts b/common/reducers/wallet.ts
index f36457b8..b9581c89 100644
--- a/common/reducers/wallet.ts
+++ b/common/reducers/wallet.ts
@@ -4,7 +4,8 @@ import {
SetWalletAction,
WalletAction,
SetWalletConfigAction,
- TypeKeys
+ TypeKeys,
+ SetTokenBalanceFulfilledAction
} from 'actions/wallet';
import { TokenValue } from 'libs/units';
import { IWallet, Balance, WalletConfig } from 'libs/wallet';
@@ -69,6 +70,30 @@ function setTokenBalancesPending(state: State): State {
};
}
+function setTokenBalancePending(state: State): State {
+ return {
+ ...state,
+ isTokensLoading: true,
+ tokensError: null
+ };
+}
+
+function setTokenBalanceFufilled(state: State, action: SetTokenBalanceFulfilledAction): State {
+ return {
+ ...state,
+ tokens: { ...state.tokens, ...action.payload },
+ isTokensLoading: false
+ };
+}
+
+function setTokenBalanceRejected(state: State): State {
+ return {
+ ...state,
+ isTokensLoading: false,
+ tokensError: 'Failed to fetch token value'
+ };
+}
+
function setTokenBalancesFulfilled(state: State, action: SetTokenBalancesFulfilledAction): State {
return {
...state,
@@ -124,6 +149,12 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
return setTokenBalancesFulfilled(state, action);
case TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED:
return setTokenBalancesRejected(state);
+ case TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING:
+ return setTokenBalancePending(state);
+ case TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED:
+ return setTokenBalanceFufilled(state, action);
+ case TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED:
+ return setTokenBalanceRejected(state);
case TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS:
return scanWalletForTokens(state);
case TypeKeys.WALLET_SET_WALLET_TOKENS:
diff --git a/common/sagas/index.ts b/common/sagas/index.ts
index 87accc80..812360aa 100644
--- a/common/sagas/index.ts
+++ b/common/sagas/index.ts
@@ -1,18 +1,33 @@
import configSaga from './config';
import deterministicWallets from './deterministicWallets';
import notifications from './notifications';
-import { bityTimeRemaining, pollBityOrderStatusSaga, postBityOrderSaga } from './swap/orders';
-import { getBityRatesSaga } from './swap/rates';
+import {
+ swapTimerSaga,
+ pollBityOrderStatusSaga,
+ postBityOrderSaga,
+ postShapeshiftOrderSaga,
+ pollShapeshiftOrderStatusSaga,
+ restartSwapSaga
+} from './swap/orders';
+import { liteSend } from './swap/liteSend';
+import { getBityRatesSaga, getShapeShiftRatesSaga, swapProviderSaga } from './swap/rates';
import wallet from './wallet';
import { transaction } from './transaction';
+
export default {
- transaction,
- bityTimeRemaining,
+ liteSend,
configSaga,
postBityOrderSaga,
+ postShapeshiftOrderSaga,
pollBityOrderStatusSaga,
+ pollShapeshiftOrderStatusSaga,
getBityRatesSaga,
+ getShapeShiftRatesSaga,
+ swapTimerSaga,
+ restartSwapSaga,
notifications,
wallet,
- deterministicWallets
+ transaction,
+ deterministicWallets,
+ swapProviderSaga
};
diff --git a/common/sagas/swap/liteSend.ts b/common/sagas/swap/liteSend.ts
new file mode 100644
index 00000000..71988813
--- /dev/null
+++ b/common/sagas/swap/liteSend.ts
@@ -0,0 +1,109 @@
+import { SagaIterator, delay } from 'redux-saga';
+import { select, put, call, take, race, fork, cancel, takeEvery } from 'redux-saga/effects';
+import { getOrigin, getPaymentAddress } from 'selectors/swap';
+import {
+ setUnitMeta,
+ setCurrentTo,
+ setCurrentValue,
+ TypeKeys as TransactionTK,
+ reset
+} from 'actions/transaction';
+import { TypeKeys as WalletTK, setTokenBalancePending } from 'actions/wallet';
+import { AppState } from 'reducers';
+import { showNotification } from 'actions/notifications';
+import { isSupportedUnit } from 'selectors/config';
+import { isEtherUnit } from 'libs/units';
+import { showLiteSend, configureLiteSend } from 'actions/swap';
+import { TypeKeys as SwapTK } from 'actions/swap/constants';
+import { isUnlocked } from 'selectors/wallet';
+
+type SwapState = AppState['swap'];
+
+export function* configureLiteSendSaga(): SagaIterator {
+ const { amount, id }: SwapState['origin'] = yield select(getOrigin);
+ const paymentAddress: SwapState['paymentAddress'] = yield call(fetchPaymentAddress);
+
+ if (!paymentAddress) {
+ yield put(showNotification('danger', 'Could not fetch payment address'));
+ return yield put(showLiteSend(false));
+ }
+
+ const supportedUnit: boolean = yield select(isSupportedUnit, id);
+ if (!supportedUnit) {
+ return yield put(showLiteSend(false));
+ }
+
+ const unlocked: boolean = yield select(isUnlocked);
+ yield put(showLiteSend(true));
+
+ // wait for wallet to be unlocked to continue
+ if (!unlocked) {
+ yield take(WalletTK.WALLET_SET);
+ }
+
+ //if it's a token, manually scan for that tokens balance and wait for it to resolve
+ if (!isEtherUnit(id)) {
+ yield put(setTokenBalancePending({ tokenSymbol: id }));
+ yield take([
+ WalletTK.WALLET_SET_TOKEN_BALANCE_FULFILLED,
+ WalletTK.WALLET_SET_TOKEN_BALANCE_REJECTED
+ ]);
+ }
+
+ yield put(setUnitMeta(id));
+ yield put(setCurrentValue(amount.toString()));
+ yield put(setCurrentTo(paymentAddress));
+}
+
+export function* handleConfigureLiteSend(): SagaIterator {
+ while (true) {
+ const liteSendProc = yield fork(configureLiteSendSaga);
+ const result = yield race({
+ transactionReset: take(TransactionTK.RESET),
+ userNavigatedAway: take(WalletTK.WALLET_RESET),
+ bityPollingFinished: take(SwapTK.SWAP_STOP_POLL_BITY_ORDER_STATUS),
+ shapeshiftPollingFinished: take(SwapTK.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
+ });
+
+ //if polling is finished we should clear state and hide this tab
+ if (result.bityPollingFinished || result.shapeshiftPollingFinished) {
+ //clear transaction state and cancel saga
+ yield cancel(liteSendProc);
+ yield put(showLiteSend(false));
+ return yield put(reset());
+ }
+
+ if (result.transactionReset) {
+ yield cancel(liteSendProc);
+ }
+
+ // if wallet reset is called, that means the user navigated away from the page, so we cancel everything
+ if (result.userNavigatedAway) {
+ yield cancel(liteSendProc);
+ yield put(showLiteSend(false));
+ return yield put(configureLiteSend());
+ }
+ // else the user just swapped to a new wallet, and we'll race against liteSend again to re-apply
+ // the same transaction parameters again
+ }
+}
+
+export function* fetchPaymentAddress(): SagaIterator {
+ const MAX_RETRIES = 5;
+ let currentTry = 0;
+ while (currentTry <= MAX_RETRIES) {
+ yield call(delay, 500);
+ const paymentAddress: SwapState['paymentAddress'] = yield select(getPaymentAddress);
+ if (paymentAddress) {
+ return paymentAddress;
+ }
+ currentTry++;
+ }
+
+ yield put(showNotification('danger', 'Payment address not found'));
+ return false;
+}
+
+export function* liteSend(): SagaIterator {
+ yield takeEvery(SwapTK.SWAP_CONFIGURE_LITE_SEND, handleConfigureLiteSend);
+}
diff --git a/common/sagas/swap/orders.ts b/common/sagas/swap/orders.ts
index 057c26fc..38786d7c 100644
--- a/common/sagas/swap/orders.ts
+++ b/common/sagas/swap/orders.ts
@@ -4,41 +4,72 @@ import {
BityOrderCreateRequestedSwapAction,
bityOrderCreateSucceededSwap,
changeStepSwap,
- orderStatusRequestedSwap,
- orderStatusSucceededSwap,
orderTimeSwap,
startOrderTimerSwap,
startPollBityOrderStatus,
stopLoadBityRatesSwap,
- stopPollBityOrderStatus
+ stopPollBityOrderStatus,
+ shapeshiftOrderStatusSucceededSwap,
+ ShapeshiftOrderCreateRequestedSwapAction,
+ stopLoadShapeshiftRatesSwap,
+ shapeshiftOrderCreateFailedSwap,
+ shapeshiftOrderCreateSucceededSwap,
+ startPollShapeshiftOrderStatus,
+ stopPollShapeshiftOrderStatus,
+ bityOrderStatusRequested,
+ stopOrderTimerSwap,
+ bityOrderStatusSucceededSwap,
+ shapeshiftOrderStatusRequested,
+ loadShapeshiftRatesRequestedSwap
} from 'actions/swap';
import { getOrderStatus, postOrder } from 'api/bity';
import moment from 'moment';
import { AppState } from 'reducers';
import { State as SwapState } from 'reducers/swap';
import { delay, SagaIterator } from 'redux-saga';
-import { call, cancel, cancelled, fork, put, select, take, takeEvery } from 'redux-saga/effects';
+import {
+ call,
+ cancel,
+ apply,
+ cancelled,
+ fork,
+ put,
+ select,
+ take,
+ takeEvery
+} from 'redux-saga/effects';
+import shapeshift from 'api/shapeshift';
+import { TypeKeys } from 'actions/swap/constants';
+import { resetWallet } from 'actions/wallet';
+import { reset } from 'actions/transaction';
export const getSwap = (state: AppState): SwapState => state.swap;
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
-export const BITY_TIMEOUT_MESSAGE = `
+export const ORDER_TIMEOUT_MESSAGE = `
Time has run out.
If you have already sent, please wait 1 hour.
If your order has not be processed after 1 hour,
please press the orange 'Issue with your Swap?' button.
`;
+export const ORDER_RECEIVED_MESSAGE = `
+ The order was recieved.
+ It may take some time to process the transaction.
+ Please wait 1 hour. If your order has not been processed by then,
+ please press the orange 'Issue with your Swap?' button.
+`;
+
export function* pollBityOrderStatus(): SagaIterator {
try {
let swap = yield select(getSwap);
while (true) {
- yield put(orderStatusRequestedSwap());
+ yield put(bityOrderStatusRequested());
const orderStatus = yield call(getOrderStatus, swap.orderId);
if (orderStatus.error) {
yield put(showNotification('danger', `Bity Error: ${orderStatus.msg}`, TEN_SECONDS));
} else {
- yield put(orderStatusSucceededSwap(orderStatus.data));
+ yield put(bityOrderStatusSucceededSwap(orderStatus.data));
yield call(delay, ONE_SECOND * 5);
swap = yield select(getSwap);
if (swap === 'CANC') {
@@ -54,18 +85,51 @@ export function* pollBityOrderStatus(): SagaIterator {
}
}
+export function* pollShapeshiftOrderStatus(): SagaIterator {
+ try {
+ let swap = yield select(getSwap);
+ while (true) {
+ yield put(shapeshiftOrderStatusRequested());
+ const orderStatus = yield apply(shapeshift, shapeshift.checkStatus, [swap.paymentAddress]);
+ if (orderStatus.status === 'failed') {
+ yield put(showNotification('danger', `Shapeshift Error: ${orderStatus.error}`, Infinity));
+ yield put(stopPollShapeshiftOrderStatus());
+ } else {
+ yield put(shapeshiftOrderStatusSucceededSwap(orderStatus));
+ yield call(delay, ONE_SECOND * 5);
+ swap = yield select(getSwap);
+ if (swap === 'CANC') {
+ break;
+ }
+ }
+ }
+ } finally {
+ if (yield cancelled()) {
+ // Request canclled
+ }
+ }
+}
+
export function* pollBityOrderStatusSaga(): SagaIterator {
- while (yield take('SWAP_START_POLL_BITY_ORDER_STATUS')) {
+ while (yield take(TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS)) {
// starts the task in the background
const pollBityOrderStatusTask = yield fork(pollBityOrderStatus);
// wait for the user to get to point where refresh is no longer needed
- yield take('SWAP_STOP_POLL_BITY_ORDER_STATUS');
+ yield take(TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS);
// cancel the background task
// this will cause the forked loadBityRates task to jump into its finally block
yield cancel(pollBityOrderStatusTask);
}
}
+export function* pollShapeshiftOrderStatusSaga(): SagaIterator {
+ while (yield take(TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS)) {
+ const pollShapeshiftOrderStatusTask = yield fork(pollShapeshiftOrderStatus);
+ yield take(TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS);
+ yield cancel(pollShapeshiftOrderStatusTask);
+ }
+}
+
export function* postBityOrderCreate(action: BityOrderCreateRequestedSwapAction): SagaIterator {
const payload = action.payload;
try {
@@ -97,17 +161,63 @@ export function* postBityOrderCreate(action: BityOrderCreateRequestedSwapAction)
}
}
-export function* postBityOrderSaga(): SagaIterator {
- yield takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate);
+export function* postShapeshiftOrderCreate(
+ action: ShapeshiftOrderCreateRequestedSwapAction
+): SagaIterator {
+ const payload = action.payload;
+ try {
+ yield put(stopLoadShapeshiftRatesSwap());
+ const order = yield apply(shapeshift, shapeshift.sendAmount, [
+ payload.withdrawal,
+ payload.originKind,
+ payload.destinationKind,
+ payload.destinationAmount
+ ]);
+ if (order.error) {
+ yield put(showNotification('danger', `Shapeshift Error: ${order.error}`, TEN_SECONDS));
+ yield put(shapeshiftOrderCreateFailedSwap());
+ } else {
+ yield put(shapeshiftOrderCreateSucceededSwap(order.success));
+ yield put(changeStepSwap(3));
+ // start countdown
+ yield put(startOrderTimerSwap());
+ // start shapeshift order status polling
+ yield put(startPollShapeshiftOrderStatus());
+ }
+ } catch (e) {
+ const message =
+ 'Connection Error. Please check the developer console for more details and/or contact support';
+ yield put(showNotification('danger', message, TEN_SECONDS));
+ yield put(shapeshiftOrderCreateFailedSwap());
+ }
}
-export function* bityTimeRemaining(): SagaIterator {
- while (yield take('SWAP_ORDER_START_TIMER')) {
+export function* postBityOrderSaga(): SagaIterator {
+ yield takeEvery(TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED, postBityOrderCreate);
+}
+
+export function* postShapeshiftOrderSaga(): SagaIterator {
+ yield takeEvery(TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED, postShapeshiftOrderCreate);
+}
+
+export function* restartSwap() {
+ yield put(reset());
+ yield put(resetWallet());
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopPollBityOrderStatus());
+ yield put(loadShapeshiftRatesRequestedSwap());
+}
+
+export function* restartSwapSaga(): SagaIterator {
+ yield takeEvery(TypeKeys.SWAP_RESTART, restartSwap);
+}
+
+export function* bityOrderTimeRemaining(): SagaIterator {
+ while (true) {
let hasShownNotification = false;
while (true) {
yield call(delay, ONE_SECOND);
const swap = yield select(getSwap);
- // if (swap.bityOrder.status === 'OPEN') {
const createdTimeStampMoment = moment(swap.orderTimestampCreatedISOString);
const validUntil = moment(createdTimeStampMoment).add(swap.validFor, 's');
const now = moment();
@@ -115,38 +225,136 @@ export function* bityTimeRemaining(): SagaIterator {
const duration = moment.duration(validUntil.diff(now));
const seconds = duration.asSeconds();
yield put(orderTimeSwap(parseInt(seconds.toString(), 10)));
- // TODO (!Important) - check orderStatus here and stop polling / show notifications based on status
+
+ switch (swap.bityOrderStatus) {
+ case 'CANC':
+ yield put(stopPollBityOrderStatus());
+ yield put(stopLoadBityRatesSwap());
+ yield put(stopOrderTimerSwap());
+ if (!hasShownNotification) {
+ hasShownNotification = true;
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
+ }
+ break;
+ case 'FILL':
+ yield put(stopPollBityOrderStatus());
+ yield put(stopLoadBityRatesSwap());
+ yield put(stopOrderTimerSwap());
+ break;
+ }
} else {
- switch (swap.orderStatus) {
+ switch (swap.bityOrderStatus) {
case 'OPEN':
yield put(orderTimeSwap(0));
yield put(stopPollBityOrderStatus());
- yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
+ yield put(stopLoadBityRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
- yield put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity));
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'CANC':
yield put(stopPollBityOrderStatus());
- yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
+ yield put(stopLoadBityRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
- yield put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity));
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'RCVE':
if (!hasShownNotification) {
hasShownNotification = true;
- yield put(showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity));
+ yield put(showNotification('warning', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'FILL':
yield put(stopPollBityOrderStatus());
- yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
+ yield put(stopLoadBityRatesSwap());
+ yield put(stopOrderTimerSwap());
break;
}
}
}
}
}
+
+export function* shapeshiftOrderTimeRemaining(): SagaIterator {
+ while (true) {
+ let hasShownNotification = false;
+ while (true) {
+ yield call(delay, ONE_SECOND);
+ const swap = yield select(getSwap);
+ const createdTimeStampMoment = moment(swap.orderTimestampCreatedISOString);
+ const validUntil = moment(createdTimeStampMoment).add(swap.validFor, 's');
+ const now = moment();
+ if (validUntil.isAfter(now)) {
+ const duration = moment.duration(validUntil.diff(now));
+ const seconds = duration.asSeconds();
+ yield put(orderTimeSwap(parseInt(seconds.toString(), 10)));
+ switch (swap.shapeshiftOrderStatus) {
+ case 'failed':
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopLoadShapeshiftRatesSwap());
+ yield put(stopOrderTimerSwap());
+ if (!hasShownNotification) {
+ hasShownNotification = true;
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
+ }
+ break;
+ case 'complete':
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopLoadShapeshiftRatesSwap());
+ yield put(stopOrderTimerSwap());
+ break;
+ }
+ } else {
+ switch (swap.shapeshiftOrderStatus) {
+ case 'no_deposits':
+ yield put(orderTimeSwap(0));
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopLoadShapeshiftRatesSwap());
+ if (!hasShownNotification) {
+ hasShownNotification = true;
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
+ }
+ break;
+ case 'failed':
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopLoadShapeshiftRatesSwap());
+ if (!hasShownNotification) {
+ hasShownNotification = true;
+ yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
+ }
+ break;
+ case 'received':
+ if (!hasShownNotification) {
+ hasShownNotification = true;
+ yield put(showNotification('warning', ORDER_RECEIVED_MESSAGE, Infinity));
+ }
+ break;
+ case 'complete':
+ yield put(stopPollShapeshiftOrderStatus());
+ yield put(stopLoadShapeshiftRatesSwap());
+ yield put(stopOrderTimerSwap());
+ break;
+ }
+ }
+ }
+ }
+}
+
+export function* handleOrderTimeRemaining(): SagaIterator {
+ const swap = yield select(getSwap);
+ let orderTimeRemainingTask;
+ if (swap.provider === 'shapeshift') {
+ orderTimeRemainingTask = yield fork(shapeshiftOrderTimeRemaining);
+ } else {
+ orderTimeRemainingTask = yield fork(bityOrderTimeRemaining);
+ }
+ yield take(TypeKeys.SWAP_ORDER_STOP_TIMER);
+ yield cancel(orderTimeRemainingTask);
+}
+
+export function* swapTimerSaga(): SagaIterator {
+ yield takeEvery(TypeKeys.SWAP_ORDER_START_TIMER, handleOrderTimeRemaining);
+}
diff --git a/common/sagas/swap/rates.ts b/common/sagas/swap/rates.ts
index f5532ddc..04509b0b 100644
--- a/common/sagas/swap/rates.ts
+++ b/common/sagas/swap/rates.ts
@@ -1,11 +1,19 @@
import { showNotification } from 'actions/notifications';
-import { loadBityRatesSucceededSwap } from 'actions/swap';
+import {
+ loadBityRatesSucceededSwap,
+ loadShapeshiftRatesSucceededSwap,
+ changeSwapProvider,
+ ChangeProviderSwapAcion
+} from 'actions/swap';
import { TypeKeys } from 'actions/swap/constants';
import { getAllRates } from 'api/bity';
import { delay, SagaIterator } from 'redux-saga';
-import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
+import { call, select, cancel, fork, put, take, takeLatest, race } from 'redux-saga/effects';
+import shapeshift from 'api/shapeshift';
+import { getSwap } from 'sagas/swap/orders';
const POLLING_CYCLE = 30000;
+export const SHAPESHIFT_TIMEOUT = 10000;
export function* loadBityRates(): SagaIterator {
while (true) {
@@ -19,6 +27,37 @@ export function* loadBityRates(): SagaIterator {
}
}
+export function* loadShapeshiftRates(): SagaIterator {
+ while (true) {
+ try {
+ // Race b/w api call and timeout
+ // getShapeShiftRates should be an api call that accepts a whitelisted arr of symbols
+ const { tokens } = yield race({
+ tokens: call(shapeshift.getAllRates),
+ timeout: call(delay, SHAPESHIFT_TIMEOUT)
+ });
+ // If tokens exist, put it into the redux state, otherwise switch to bity.
+ if (tokens) {
+ yield put(loadShapeshiftRatesSucceededSwap(tokens));
+ } else {
+ yield put(
+ showNotification('danger', 'Error loading ShapeShift tokens - reverting to Bity')
+ );
+ }
+ } catch (error) {
+ yield put(showNotification('danger', `Error loading ShapeShift tokens - ${error}`));
+ }
+ yield call(delay, POLLING_CYCLE);
+ }
+}
+
+export function* swapProvider(action: ChangeProviderSwapAcion): SagaIterator {
+ const swap = yield select(getSwap);
+ if (swap.provider !== action.payload) {
+ yield put(changeSwapProvider(action.payload));
+ }
+}
+
// Fork our recurring API call, watch for the need to cancel.
export function* handleBityRates(): SagaIterator {
const loadBityRatesTask = yield fork(loadBityRates);
@@ -30,3 +69,20 @@ export function* handleBityRates(): SagaIterator {
export function* getBityRatesSaga(): SagaIterator {
yield takeLatest(TypeKeys.SWAP_LOAD_BITY_RATES_REQUESTED, handleBityRates);
}
+
+// Fork our API call
+export function* handleShapeShiftRates(): SagaIterator {
+ const loadShapeShiftRatesTask = yield fork(loadShapeshiftRates);
+ yield take(TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES);
+ yield cancel(loadShapeShiftRatesTask);
+}
+
+// Watch for SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED action.
+export function* getShapeShiftRatesSaga(): SagaIterator {
+ yield takeLatest(TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED, handleShapeShiftRates);
+}
+
+// Watch for provider swaps
+export function* swapProviderSaga(): SagaIterator {
+ yield takeLatest(TypeKeys.SWAP_CHANGE_PROVIDER, swapProvider);
+}
diff --git a/common/sagas/transaction/network/gas.ts b/common/sagas/transaction/network/gas.ts
index e11a92e6..91963277 100644
--- a/common/sagas/transaction/network/gas.ts
+++ b/common/sagas/transaction/network/gas.ts
@@ -60,8 +60,8 @@ export function* estimateGas(): SagaIterator {
while (true) {
const { payload }: EstimateGasRequestedAction = yield take(requestChan);
- // debounce 1000 ms
- yield call(delay, 1000);
+ // debounce 250 ms
+ yield call(delay, 250);
const node: INode = yield select(getNodeLib);
const walletInst: IWallet = yield select(getWalletInst);
try {
diff --git a/common/sagas/wallet/wallet.ts b/common/sagas/wallet/wallet.ts
index 3a8764ae..dee1dc1a 100644
--- a/common/sagas/wallet/wallet.ts
+++ b/common/sagas/wallet/wallet.ts
@@ -13,7 +13,10 @@ import {
UnlockPrivateKeyAction,
ScanWalletForTokensAction,
SetWalletTokensAction,
- TypeKeys
+ TypeKeys,
+ SetTokenBalancePendingAction,
+ setTokenBalanceFulfilled,
+ setTokenBalanceRejected
} from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
@@ -27,10 +30,10 @@ import {
Web3Wallet,
WalletConfig
} from 'libs/wallet';
-import { NODES, initWeb3Node } from 'config/data';
+import { NODES, initWeb3Node, Token } from 'config/data';
import { SagaIterator } from 'redux-saga';
import { apply, call, fork, put, select, takeEvery, take } from 'redux-saga/effects';
-import { getNodeLib } from 'selectors/config';
+import { getNodeLib, getAllTokens } from 'selectors/config';
import {
getTokens,
getWalletInst,
@@ -80,6 +83,30 @@ export function* updateTokenBalances(): SagaIterator {
}
}
+export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
+ try {
+ const wallet: null | IWallet = yield select(getWalletInst);
+ const { tokenSymbol } = action.payload;
+ const allTokens: Token[] = yield select(getAllTokens);
+ const token = allTokens.find(t => t.symbol === tokenSymbol);
+
+ if (!wallet) {
+ return;
+ }
+
+ if (!token) {
+ throw Error('Token not found');
+ }
+
+ const tokenBalances: TokenBalanceLookup = yield call(getTokenBalances, wallet, [token]);
+
+ yield put(setTokenBalanceFulfilled(tokenBalances));
+ } catch (error) {
+ console.error('Failed to get token balance', error);
+ yield put(setTokenBalanceRejected());
+ }
+}
+
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
try {
const wallet = action.payload;
@@ -229,6 +256,7 @@ export default function* walletSaga(): SagaIterator {
takeEvery(TypeKeys.WALLET_SET, handleNewWallet),
takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens),
takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens),
- takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd)
+ takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd),
+ takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance)
];
}
diff --git a/common/selectors/config.ts b/common/selectors/config.ts
index fb76416c..a2004891 100644
--- a/common/selectors/config.ts
+++ b/common/selectors/config.ts
@@ -9,6 +9,8 @@ import {
import { INode } from 'libs/nodes/INode';
import { AppState } from 'reducers';
import { getNetworkConfigFromId } from 'utils/network';
+import { isEtherUnit } from 'libs/units';
+import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift';
export function getNode(state: AppState): string {
return state.config.nodeSelection;
@@ -41,6 +43,12 @@ export function getAllTokens(state: AppState): Token[] {
return networkTokens.concat(state.customTokens);
}
+export function tokenExists(state: AppState, token: string): boolean {
+ const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token);
+ const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token);
+ return existsInNetwork || existInWhitelist;
+}
+
export function getLanguageSelection(state: AppState): string {
return state.config.languageSelection;
}
@@ -62,3 +70,12 @@ export function getForceOffline(state: AppState): boolean {
}
export const isAnyOffline = (state: AppState) => getOffline(state) || getForceOffline(state);
+
+export function isSupportedUnit(state: AppState, unit: string) {
+ const isToken: boolean = tokenExists(state, unit);
+ const isEther: boolean = isEtherUnit(unit);
+ if (!isToken && !isEther) {
+ return false;
+ }
+ return true;
+}
diff --git a/common/selectors/swap.ts b/common/selectors/swap.ts
new file mode 100644
index 00000000..a135de7e
--- /dev/null
+++ b/common/selectors/swap.ts
@@ -0,0 +1,6 @@
+import { AppState } from 'reducers';
+
+const getSwap = (state: AppState) => state.swap;
+export const getOrigin = (state: AppState) => getSwap(state).origin;
+export const getPaymentAddress = (state: AppState) => getSwap(state).paymentAddress;
+export const shouldDisplayLiteSend = (state: AppState) => getSwap(state).showLiteSend;
diff --git a/common/selectors/wallet.ts b/common/selectors/wallet.ts
index ca35dac0..90f9f4cd 100644
--- a/common/selectors/wallet.ts
+++ b/common/selectors/wallet.ts
@@ -76,11 +76,15 @@ export function getTokenBalances(state: AppState, nonZeroOnly: boolean = false):
}
export const getTokenBalance = (state: AppState, unit: string): TokenValue | null => {
- return getTokenWithBalance(state, unit).balance;
+ const token = getTokenWithBalance(state, unit);
+ if (!token) {
+ return token;
+ }
+ return token.balance;
};
export const getTokenWithBalance = (state: AppState, unit: string): TokenBalance => {
- const tokens = getTokenBalances(state, true);
+ const tokens = getTokenBalances(state, false);
const currentToken = tokens.filter(t => t.symbol === unit);
//TODO: getting the first index is kinda hacky
return currentToken[0];
diff --git a/common/store.ts b/common/store.ts
index ace85b5a..fa97bba3 100644
--- a/common/store.ts
+++ b/common/store.ts
@@ -130,8 +130,18 @@ const configureStore = () => {
},
swap: {
...state.swap,
- options: {},
- bityRates: {}
+ options: {
+ byId: {},
+ allIds: []
+ },
+ bityRates: {
+ byId: {},
+ allIds: []
+ },
+ shapeshiftRates: {
+ byId: {},
+ allIds: []
+ }
},
customTokens: state.customTokens
});
diff --git a/common/utils/helpers.ts b/common/utils/helpers.ts
index 16ad445f..82f561ae 100644
--- a/common/utils/helpers.ts
+++ b/common/utils/helpers.ts
@@ -1,3 +1,11 @@
+import has from 'lodash/has';
+
+export function objectContainsObjectKeys(checkingObject, containingObject) {
+ const checkingObjectKeys = Object.keys(checkingObject);
+ const containsAll = checkingObjectKeys.map(key => has(containingObject, key));
+ return containsAll.every(isTrue => isTrue);
+}
+
export function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
diff --git a/spec/pages/__snapshots__/Swap.spec.tsx.snap b/spec/pages/__snapshots__/Swap.spec.tsx.snap
index c35cc009..4f972232 100644
--- a/spec/pages/__snapshots__/Swap.spec.tsx.snap
+++ b/spec/pages/__snapshots__/Swap.spec.tsx.snap
@@ -4,6 +4,7 @@ exports[`render snapshot 1`] = `
`;
diff --git a/spec/reducers/swap.spec.ts b/spec/reducers/swap.spec.ts
index 4872fcc4..0ec7d1e4 100644
--- a/spec/reducers/swap.spec.ts
+++ b/spec/reducers/swap.spec.ts
@@ -1,11 +1,39 @@
import { swap, INITIAL_STATE } from 'reducers/swap';
import * as swapActions from 'actions/swap';
-import { NormalizedBityRates, NormalizedOptions } from 'reducers/swap/types';
+import {
+ NormalizedBityRates,
+ NormalizedOptions,
+ NormalizedShapeshiftRates
+} from 'reducers/swap/types';
import { normalize } from 'normalizr';
import * as schema from 'reducers/swap/schema';
+import { TypeKeys } from 'actions/swap/constants';
describe('swap reducer', () => {
- const apiResponse = {
+ const shapeshiftApiResponse = {
+ ['1SSTANT']: {
+ id: '1STANT',
+ options: [
+ {
+ id: '1ST',
+ status: 'available',
+ image: 'https://shapeshift.io/images/coins/firstblood.png',
+ name: 'FirstBlood'
+ },
+ {
+ id: 'ANT',
+ status: 'available',
+ image: 'https://shapeshift.io/images/coins/aragon.png',
+ name: 'Aragon'
+ }
+ ],
+ rate: '0.24707537',
+ limit: 5908.29166225,
+ min: 7.86382979
+ }
+ };
+
+ const bityApiResponse = {
BTCETH: {
id: 'BTCETH',
options: [{ id: 'BTC' }, { id: 'ETH' }],
@@ -17,20 +45,43 @@ describe('swap reducer', () => {
rate: 0.042958
}
};
- const normalizedbityRates: NormalizedBityRates = {
- byId: normalize(apiResponse, [schema.bityRate]).entities.bityRates,
- allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.bityRates)
+
+ const normalizedBityRates: NormalizedBityRates = {
+ byId: normalize(bityApiResponse, [schema.providerRate]).entities.providerRates,
+ allIds: schema.allIds(normalize(bityApiResponse, [schema.providerRate]).entities.providerRates)
};
- const normalizedOptions: NormalizedOptions = {
- byId: normalize(apiResponse, [schema.bityRate]).entities.options,
- allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.options)
+ const normalizedShapeshiftRates: NormalizedShapeshiftRates = {
+ byId: normalize(shapeshiftApiResponse, [schema.providerRate]).entities.providerRates,
+ allIds: schema.allIds(
+ normalize(shapeshiftApiResponse, [schema.providerRate]).entities.providerRates
+ )
};
+ const normalizedBityOptions: NormalizedOptions = {
+ byId: normalize(bityApiResponse, [schema.providerRate]).entities.options,
+ allIds: schema.allIds(normalize(bityApiResponse, [schema.providerRate]).entities.options)
+ };
+ const normalizedShapeshiftOptions: NormalizedOptions = {
+ byId: normalize(shapeshiftApiResponse, [schema.providerRate]).entities.options,
+ allIds: schema.allIds(normalize(shapeshiftApiResponse, [schema.providerRate]).entities.options)
+ };
+
it('should handle SWAP_LOAD_BITY_RATES_SUCCEEDED', () => {
- expect(swap(undefined, swapActions.loadBityRatesSucceededSwap(apiResponse))).toEqual({
+ expect(swap(undefined, swapActions.loadBityRatesSucceededSwap(bityApiResponse))).toEqual({
...INITIAL_STATE,
isFetchingRates: false,
- bityRates: normalizedbityRates,
- options: normalizedOptions
+ bityRates: normalizedBityRates,
+ options: normalizedBityOptions
+ });
+ });
+
+ it('should handle SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED', () => {
+ expect(
+ swap(undefined, swapActions.loadShapeshiftRatesSucceededSwap(shapeshiftApiResponse))
+ ).toEqual({
+ ...INITIAL_STATE,
+ isFetchingRates: false,
+ shapeshiftRates: normalizedShapeshiftRates,
+ options: normalizedShapeshiftOptions
});
});
@@ -55,7 +106,8 @@ describe('swap reducer', () => {
swap(
{
...INITIAL_STATE,
- bityRates: normalizedbityRates,
+ bityRates: normalizedBityRates,
+ shapeshiftRates: normalizedShapeshiftRates,
origin: { id: 'BTC', amount: 1 },
destination: { id: 'ETH', amount: 3 }
},
@@ -63,14 +115,15 @@ describe('swap reducer', () => {
)
).toEqual({
...INITIAL_STATE,
- bityRates: normalizedbityRates
+ bityRates: normalizedBityRates,
+ shapeshiftRates: normalizedShapeshiftRates
});
});
- it('should handle SWAP_ORDER_CREATE_REQUESTED', () => {
+ it('should handle SWAP_BITY_ORDER_CREATE_REQUESTED', () => {
expect(
swap(undefined, {
- type: 'SWAP_ORDER_CREATE_REQUESTED'
+ type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
@@ -78,10 +131,32 @@ describe('swap reducer', () => {
});
});
- it('should handle SWAP_ORDER_CREATE_FAILED', () => {
+ it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED', () => {
expect(
swap(undefined, {
- type: 'SWAP_ORDER_CREATE_FAILED'
+ type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED
+ } as swapActions.SwapAction)
+ ).toEqual({
+ ...INITIAL_STATE,
+ isPostingOrder: true
+ });
+ });
+
+ it('should handle SWAP_BITY_ORDER_CREATE_FAILED', () => {
+ expect(
+ swap(undefined, {
+ type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED
+ } as swapActions.SwapAction)
+ ).toEqual({
+ ...INITIAL_STATE,
+ isPostingOrder: false
+ });
+ });
+
+ it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_FAILED', () => {
+ expect(
+ swap(undefined, {
+ type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
@@ -122,11 +197,49 @@ describe('swap reducer', () => {
validFor: mockedBityOrder.validFor,
orderTimestampCreatedISOString: mockedBityOrder.timestamp_created,
paymentAddress: mockedBityOrder.payment_address,
- orderStatus: mockedBityOrder.status,
+ bityOrderStatus: mockedBityOrder.status,
orderId: mockedBityOrder.id
});
});
+ it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED', () => {
+ const mockedShapeshiftOrder: swapActions.ShapeshiftOrderResponse = {
+ orderId: '64d73218-0ee9-4c6c-9bbd-6da9208595f5',
+ pair: 'eth_ant',
+ withdrawal: '0x6b3a639eb96d8e0241fe4e114d99e739f906944e',
+ withdrawalAmount: '200.13550988',
+ deposit: '0x039ed77933388642fdd618d27bfc4fa3582d10c4',
+ depositAmount: '0.98872802',
+ expiration: 1514633757288,
+ quotedRate: '203.47912271',
+ maxLimit: 7.04575258,
+ apiPubKey:
+ '0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160',
+ minerFee: '1.05'
+ };
+
+ const swapState = swap(
+ undefined,
+ swapActions.shapeshiftOrderCreateSucceededSwap(mockedShapeshiftOrder)
+ );
+
+ expect(swapState).toEqual({
+ ...INITIAL_STATE,
+ shapeshiftOrder: {
+ ...mockedShapeshiftOrder
+ },
+ isPostingOrder: false,
+ originAmount: parseFloat(mockedShapeshiftOrder.depositAmount),
+ destinationAmount: parseFloat(mockedShapeshiftOrder.withdrawalAmount),
+ secondsRemaining: swapState.secondsRemaining,
+ validFor: swapState.validFor,
+ orderTimestampCreatedISOString: swapState.orderTimestampCreatedISOString,
+ paymentAddress: mockedShapeshiftOrder.deposit,
+ shapeshiftOrderStatus: 'no_deposits',
+ orderId: mockedShapeshiftOrder.orderId
+ });
+ });
+
it('should handle SWAP_BITY_ORDER_STATUS_SUCCEEDED', () => {
const mockedBityResponse: swapActions.BityOrderResponse = {
input: {
@@ -144,10 +257,25 @@ describe('swap reducer', () => {
status: 'status'
};
- expect(swap(undefined, swapActions.orderStatusSucceededSwap(mockedBityResponse))).toEqual({
+ expect(swap(undefined, swapActions.bityOrderStatusSucceededSwap(mockedBityResponse))).toEqual({
...INITIAL_STATE,
outputTx: mockedBityResponse.output.reference,
- orderStatus: mockedBityResponse.output.status
+ bityOrderStatus: mockedBityResponse.output.status
+ });
+ });
+
+ it('should handle SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED', () => {
+ const mockedShapeshiftResponse: swapActions.ShapeshiftStatusResponse = {
+ status: 'complete',
+ transaction: '0x039ed77933388642fdd618d27bfc4fa3582d10c4'
+ };
+
+ expect(
+ swap(undefined, swapActions.shapeshiftOrderStatusSucceededSwap(mockedShapeshiftResponse))
+ ).toEqual({
+ ...INITIAL_STATE,
+ shapeshiftOrderStatus: mockedShapeshiftResponse.status,
+ outputTx: mockedShapeshiftResponse.transaction
});
});
@@ -170,6 +298,17 @@ describe('swap reducer', () => {
});
});
+ it('should handle SWAP_LOAD_SHAPESHIFT_RATE_REQUESTED', () => {
+ expect(
+ swap(undefined, {
+ type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED
+ } as swapActions.SwapAction)
+ ).toEqual({
+ ...INITIAL_STATE,
+ isFetchingRates: true
+ });
+ });
+
it('should handle SWAP_STOP_LOAD_BITY_RATES', () => {
expect(
swap(undefined, {
@@ -180,4 +319,15 @@ describe('swap reducer', () => {
isFetchingRates: false
});
});
+
+ it('should handle SWAP_STOP_LOAD_SHAPESHIFT_RATES', () => {
+ expect(
+ swap(undefined, {
+ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES
+ } as swapActions.SwapAction)
+ ).toEqual({
+ ...INITIAL_STATE,
+ isFetchingRates: false
+ });
+ });
});
diff --git a/spec/sagas/swap/liteSend.spec.ts b/spec/sagas/swap/liteSend.spec.ts
new file mode 100644
index 00000000..00c77b57
--- /dev/null
+++ b/spec/sagas/swap/liteSend.spec.ts
@@ -0,0 +1,34 @@
+import { configuredStore } from 'store';
+import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
+import { take, race, fork } from 'redux-saga/effects';
+import { TypeKeys as TransactionTK } from 'actions/transaction';
+import { TypeKeys as WalletTK } from 'actions/wallet';
+import { TypeKeys as SwapTK } from 'actions/swap/constants';
+import { configureLiteSendSaga, handleConfigureLiteSend } from 'sagas/swap/liteSend';
+
+// init module
+configuredStore.getState();
+
+describe('Testing handle configure lite send', () => {
+ const generators = {
+ original: cloneableGenerator(handleConfigureLiteSend)()
+ };
+ const { original } = generators;
+
+ it('forks a configureLiteSend saga', () => {
+ const expectedYield = fork(configureLiteSendSaga);
+ expect(original.next().value).toEqual(expectedYield);
+ });
+
+ it('races between three conditions, either the transaction state is reset, the user navigated away from the page, or bitty/shapeshift polling as finished', () => {
+ const mockedTask = createMockTask();
+ const expectedYield = race({
+ transactionReset: take(TransactionTK.RESET),
+ userNavigatedAway: take(WalletTK.WALLET_RESET),
+ bityPollingFinished: take(SwapTK.SWAP_STOP_POLL_BITY_ORDER_STATUS),
+ shapeshiftPollingFinished: take(SwapTK.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
+ });
+
+ expect(original.next(mockedTask).value).toEqual(expectedYield);
+ });
+});
diff --git a/spec/sagas/swap/orders.spec.ts b/spec/sagas/swap/orders.spec.ts
index 5553e1d9..9f4a45b4 100644
--- a/spec/sagas/swap/orders.spec.ts
+++ b/spec/sagas/swap/orders.spec.ts
@@ -8,23 +8,32 @@ import {
BityOrderOutput,
BityOrderResponse,
changeStepSwap,
- orderStatusRequestedSwap,
- orderStatusSucceededSwap,
+ bityOrderStatusRequested,
+ bityOrderStatusSucceededSwap,
orderTimeSwap,
startOrderTimerSwap,
startPollBityOrderStatus,
stopLoadBityRatesSwap,
- stopPollBityOrderStatus
+ stopPollBityOrderStatus,
+ startPollShapeshiftOrderStatus,
+ shapeshiftOrderStatusRequested,
+ shapeshiftOrderStatusSucceededSwap,
+ shapeshiftOrderCreateRequestedSwap,
+ shapeshiftOrderCreateSucceededSwap,
+ shapeshiftOrderCreateFailedSwap,
+ stopLoadShapeshiftRatesSwap,
+ ShapeshiftOrderResponse,
+ stopPollShapeshiftOrderStatus,
+ stopOrderTimerSwap
} from 'actions/swap';
import { getOrderStatus, postOrder } from 'api/bity';
-import {
- State as SwapState,
- INITIAL_STATE as INITIAL_SWAP_STATE
-} from 'reducers/swap';
+import shapeshift from 'api/shapeshift';
+import { State as SwapState, INITIAL_STATE as INITIAL_SWAP_STATE } from 'reducers/swap';
import { delay } from 'redux-saga';
import {
call,
cancel,
+ apply,
cancelled,
fork,
put,
@@ -38,10 +47,17 @@ import {
pollBityOrderStatusSaga,
postBityOrderCreate,
postBityOrderSaga,
- bityTimeRemaining,
- BITY_TIMEOUT_MESSAGE
+ pollShapeshiftOrderStatus,
+ pollShapeshiftOrderStatusSaga,
+ postShapeshiftOrderSaga,
+ shapeshiftOrderTimeRemaining,
+ bityOrderTimeRemaining,
+ ORDER_TIMEOUT_MESSAGE,
+ postShapeshiftOrderCreate,
+ ORDER_RECEIVED_MESSAGE
} from 'sagas/swap/orders';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
+import { TypeKeys } from 'actions/swap/constants';
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
@@ -96,34 +112,24 @@ describe('pollBityOrderStatus*', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
- it('should put orderStatusRequestedSwap', () => {
- expect(data.gen.next(fakeSwap).value).toEqual(
- put(orderStatusRequestedSwap())
- );
+ it('should put bityOrderStatusRequestedSwap', () => {
+ expect(data.gen.next(fakeSwap).value).toEqual(put(bityOrderStatusRequested()));
});
it('should call getOrderStatus with swap.orderId', () => {
- expect(data.gen.next().value).toEqual(
- call(getOrderStatus, fakeSwap.orderId)
- );
+ expect(data.gen.next().value).toEqual(call(getOrderStatus, fakeSwap.orderId));
});
it('should put showNotfication on error', () => {
data.clone = data.gen.clone();
expect(data.clone.next(errorStatus).value).toEqual(
- put(
- showNotification(
- 'danger',
- `Bity Error: ${errorStatus.msg}`,
- TEN_SECONDS
- )
- )
+ put(showNotification('danger', `Bity Error: ${errorStatus.msg}`, TEN_SECONDS))
);
});
it('should put orderStatusSucceededSwap', () => {
expect(data.gen.next(successStatus).value).toEqual(
- put(orderStatusSucceededSwap(successStatus.data))
+ put(bityOrderStatusSucceededSwap(successStatus.data))
);
});
@@ -142,10 +148,81 @@ describe('pollBityOrderStatus*', () => {
});
it('should restart loop', () => {
- expect(data.gen.next(fakeSwap).value).toEqual(
- put(orderStatusRequestedSwap())
+ expect(data.gen.next(fakeSwap).value).toEqual(put(bityOrderStatusRequested()));
+ });
+});
+
+describe('pollShapeshiftOrderStatus*', () => {
+ const data = {} as any;
+ data.gen = cloneableGenerator(pollShapeshiftOrderStatus)();
+ const fakeSwap: SwapState = {
+ ...INITIAL_SWAP_STATE,
+ orderId: '1'
+ };
+ const cancelledSwap = 'CANC';
+ const successStatus = {
+ status: 'complete',
+ transaction: '0x'
+ };
+ const errorStatus = {
+ error: 'Shapeshift error',
+ status: 'failed'
+ };
+ let random;
+
+ beforeAll(() => {
+ random = Math.random;
+ Math.random = () => 0.001;
+ });
+
+ afterAll(() => {
+ Math.random = random;
+ });
+
+ it('should select getSwap', () => {
+ expect(data.gen.next().value).toEqual(select(getSwap));
+ });
+
+ it('should put shapeshiftOrderStatusRequestedSwap', () => {
+ expect(data.gen.next(fakeSwap).value).toEqual(put(shapeshiftOrderStatusRequested()));
+ });
+
+ it('should apply shapeshift.checkStatus with swap.paymentAddress', () => {
+ expect(data.gen.next().value).toEqual(
+ apply(shapeshift, shapeshift.checkStatus, [fakeSwap.paymentAddress])
);
});
+
+ it('should put showNotfication on error', () => {
+ data.clone = data.gen.clone();
+ expect(data.clone.next(errorStatus).value).toEqual(
+ put(showNotification('danger', `Shapeshift Error: ${errorStatus.error}`, Infinity))
+ );
+ });
+
+ it('should put shapeshiftOrderStatusSucceededSwap', () => {
+ expect(data.gen.next(successStatus).value).toEqual(
+ put(shapeshiftOrderStatusSucceededSwap(successStatus))
+ );
+ });
+
+ it('should call delay for 5 seconds', () => {
+ expect(data.gen.next().value).toEqual(call(delay, ONE_SECOND * 5));
+ });
+
+ it('should select getSwap', () => {
+ expect(data.gen.next().value).toEqual(select(getSwap));
+ });
+
+ it('should break loop if swap is cancelled', () => {
+ data.clone2 = data.gen.clone();
+ expect(data.clone2.next(cancelledSwap).value).toEqual(cancelled());
+ expect(data.clone2.next().done).toEqual(true);
+ });
+
+ it('should restart loop', () => {
+ expect(data.gen.next(fakeSwap).value).toEqual(put(shapeshiftOrderStatusRequested()));
+ });
});
describe('pollBityOrderStatusSaga*', () => {
@@ -154,9 +231,7 @@ describe('pollBityOrderStatusSaga*', () => {
const mockedTask = createMockTask();
it('should take SWAP_START_POLL_BITY_ORDER_STATUS', () => {
- expect(data.gen.next().value).toEqual(
- take('SWAP_START_POLL_BITY_ORDER_STATUS')
- );
+ expect(data.gen.next().value).toEqual(take(TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS));
});
it('should be done if order status is false', () => {
@@ -170,7 +245,7 @@ describe('pollBityOrderStatusSaga*', () => {
it('should take SWAP_STOP_POLL_BITY_ORDER_STATUS', () => {
expect(data.gen.next(mockedTask).value).toEqual(
- take('SWAP_STOP_POLL_BITY_ORDER_STATUS')
+ take(TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS)
);
});
@@ -179,6 +254,35 @@ describe('pollBityOrderStatusSaga*', () => {
});
});
+describe('pollShapeshiftOrderStatusSaga*', () => {
+ const data = {} as any;
+ data.gen = cloneableGenerator(pollShapeshiftOrderStatusSaga)();
+ const mockedTask = createMockTask();
+
+ it('should take SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS', () => {
+ expect(data.gen.next().value).toEqual(take(TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS));
+ });
+
+ it('should be done if order status is false', () => {
+ data.clone = data.gen.clone();
+ expect(data.clone.next(false).done).toEqual(true);
+ });
+
+ it('should fork pollShapeshiftOrderStatus', () => {
+ expect(data.gen.next(true).value).toEqual(fork(pollShapeshiftOrderStatus));
+ });
+
+ it('should take SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS', () => {
+ expect(data.gen.next(mockedTask).value).toEqual(
+ take(TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
+ );
+ });
+
+ it('should cancel pollShapeshiftOrderStatusTask', () => {
+ expect(data.gen.next().value).toEqual(cancel(mockedTask));
+ });
+});
+
describe('postBityOrderCreate*', () => {
const amount = 100;
const destinationAddress = '0x0';
@@ -252,25 +356,126 @@ describe('postBityOrderCreate*', () => {
it('should handle an errored order', () => {
expect(data.clone2.next(errorOrder).value).toEqual(
- put(
- showNotification('danger', `Bity Error: ${errorOrder.msg}`, TEN_SECONDS)
- )
+ put(showNotification('danger', `Bity Error: ${errorOrder.msg}`, TEN_SECONDS))
);
expect(data.clone2.next().value).toEqual(put(bityOrderCreateFailedSwap()));
});
});
+describe('postShapeshiftOrderCreate*', () => {
+ const amount = 100;
+ const withdrawalAddress = '0x0';
+ const originKind = 'BAT';
+ const destKind = 'ETH';
+ const action = shapeshiftOrderCreateRequestedSwap(
+ withdrawalAddress,
+ originKind,
+ destKind,
+ amount
+ );
+ const orderResp: ShapeshiftOrderResponse = {
+ deposit: '0x0',
+ depositAmount: '0',
+ expiration: 100,
+ maxLimit: 1,
+ minerFee: '0.1',
+ orderId: '1',
+ pair: 'BTC_ETH',
+ quotedRate: '1',
+ withdrawal: '0x0',
+ withdrawalAmount: '2'
+ };
+ const successOrder = { success: orderResp };
+ const errorOrder = { error: 'message' };
+ const connectionErrMsg =
+ 'Connection Error. Please check the developer console for more details and/or contact support';
+
+ const data = {} as any;
+ data.gen = cloneableGenerator(postShapeshiftOrderCreate)(action);
+
+ let random;
+ beforeAll(() => {
+ random = Math.random;
+ Math.random = () => 0.001;
+ });
+
+ afterAll(() => {
+ Math.random = random;
+ });
+
+ it('should put stopLoadShapeshiftRatesSwap', () => {
+ expect(data.gen.next().value).toEqual(put(stopLoadShapeshiftRatesSwap()));
+ });
+
+ it('should call shapeshift.sendAmount', () => {
+ data.clone1 = data.gen.clone();
+ expect(data.gen.next().value).toEqual(
+ apply(shapeshift, shapeshift.sendAmount, [
+ action.payload.withdrawal,
+ action.payload.originKind,
+ action.payload.destinationKind,
+ action.payload.destinationAmount
+ ])
+ );
+ });
+
+ it('should put shapeshiftOrderCreateSucceededSwap', () => {
+ data.clone2 = data.gen.clone();
+ expect(data.gen.next(successOrder).value).toEqual(
+ put(shapeshiftOrderCreateSucceededSwap(successOrder.success))
+ );
+ });
+
+ it('should put changeStepSwap', () => {
+ expect(data.gen.next().value).toEqual(put(changeStepSwap(3)));
+ });
+
+ it('should put startOrderTimerSwap', () => {
+ expect(data.gen.next().value).toEqual(put(startOrderTimerSwap()));
+ });
+
+ it('should put startPollShapeshiftOrderStatus', () => {
+ expect(data.gen.next().value).toEqual(put(startPollShapeshiftOrderStatus()));
+ });
+
+ // failure modes
+ it('should handle a connection exeception', () => {
+ expect(data.clone1.throw().value).toEqual(
+ put(showNotification('danger', connectionErrMsg, TEN_SECONDS))
+ );
+ expect(data.clone1.next().value).toEqual(put(shapeshiftOrderCreateFailedSwap()));
+ expect(data.clone1.next().done).toEqual(true);
+ });
+
+ it('should handle an errored order', () => {
+ expect(data.clone2.next(errorOrder).value).toEqual(
+ put(showNotification('danger', `Shapeshift Error: ${errorOrder.error}`, TEN_SECONDS))
+ );
+ expect(data.clone2.next().value).toEqual(put(shapeshiftOrderCreateFailedSwap()));
+ });
+});
+
describe('postBityOrderSaga*', () => {
const gen = postBityOrderSaga();
- it('should takeEvery SWAP_ORDER_CREATE_REQUESTED', () => {
+ it('should takeEvery SWAP_BITY_ORDER_CREATE_REQUESTED', () => {
expect(gen.next().value).toEqual(
- takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate)
+ takeEvery(TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED, postBityOrderCreate)
);
});
});
-describe('bityTimeRemaining*', () => {
+describe('postShapeshiftOrderSaga*', () => {
+ const gen = postShapeshiftOrderSaga();
+
+ it('should takeEvery SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED', () => {
+ expect(gen.next().value).toEqual(
+ takeEvery(TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED, postShapeshiftOrderCreate)
+ );
+ });
+});
+
+describe('bityOrderTimeRemaining*', () => {
const orderTime = new Date().toISOString();
const orderTimeExpired = new Date().getTime() - ELEVEN_SECONDS;
const swapValidFor = 10; //seconds
@@ -287,7 +492,7 @@ describe('bityTimeRemaining*', () => {
let random;
const data = {} as any;
- data.gen = cloneableGenerator(bityTimeRemaining)();
+ data.gen = cloneableGenerator(bityOrderTimeRemaining)();
beforeAll(() => {
random = Math.random;
@@ -298,15 +503,6 @@ describe('bityTimeRemaining*', () => {
Math.random = random;
});
- it('should take SWAP_ORDER_START_TIMER', () => {
- expect(data.gen.next().value).toEqual(take('SWAP_ORDER_START_TIMER'));
- });
-
- it('should break while loop when take SWAP_ORDER_START_TIMER is false', () => {
- data.clone1 = data.gen.clone();
- expect(data.clone1.next().done).toEqual(true);
- });
-
it('should call delay of one second', () => {
expect(data.gen.next(true).value).toEqual(call(delay, ONE_SECOND));
});
@@ -324,48 +520,122 @@ describe('bityTimeRemaining*', () => {
});
it('should handle an OPEN order state', () => {
- const openOrder = { ...swapOrderExpired, orderStatus: 'OPEN' };
+ const openOrder = { ...swapOrderExpired, bityOrderStatus: 'OPEN' };
data.OPEN = data.gen.clone();
expect(data.OPEN.next(openOrder).value).toEqual(put(orderTimeSwap(0)));
expect(data.OPEN.next().value).toEqual(put(stopPollBityOrderStatus()));
+ expect(data.OPEN.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
expect(data.OPEN.next().value).toEqual(
- put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
- );
- expect(data.OPEN.next().value).toEqual(
- put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
+ put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a CANC order state', () => {
- const cancOrder = { ...swapOrderExpired, orderStatus: 'CANC' };
+ const cancOrder = { ...swapOrderExpired, bityOrderStatus: 'CANC' };
data.CANC = data.gen.clone();
- expect(data.CANC.next(cancOrder).value).toEqual(
- put(stopPollBityOrderStatus())
- );
+ expect(data.CANC.next(cancOrder).value).toEqual(put(stopPollBityOrderStatus()));
+ expect(data.CANC.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
expect(data.CANC.next().value).toEqual(
- put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
- );
- expect(data.CANC.next().value).toEqual(
- put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
+ put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a RCVE order state', () => {
- const rcveOrder = { ...swapOrderExpired, orderStatus: 'RCVE' };
+ const rcveOrder = { ...swapOrderExpired, bityOrderStatus: 'RCVE' };
data.RCVE = data.gen.clone();
expect(data.RCVE.next(rcveOrder).value).toEqual(
- put(showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity))
+ put(showNotification('warning', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a FILL order state', () => {
- const fillOrder = { ...swapOrderExpired, orderStatus: 'FILL' };
+ const fillOrder = { ...swapOrderExpired, bityOrderStatus: 'FILL' };
data.FILL = data.gen.clone();
- expect(data.FILL.next(fillOrder).value).toEqual(
- put(stopPollBityOrderStatus())
- );
- expect(data.FILL.next().value).toEqual(
- put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
- );
+ expect(data.FILL.next(fillOrder).value).toEqual(put(stopPollBityOrderStatus()));
+ expect(data.FILL.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
+ });
+});
+
+describe('shapeshiftOrderTimeRemaining*', () => {
+ const orderTime = new Date().toISOString();
+ const orderTimeExpired = new Date().getTime() - ELEVEN_SECONDS;
+ const swapValidFor = 10; //seconds
+ const swapOrder = {
+ ...INITIAL_SWAP_STATE,
+ orderTimestampCreatedISOString: orderTime,
+ validFor: swapValidFor
+ };
+ const swapOrderExpired = {
+ ...INITIAL_SWAP_STATE,
+ orderTimestampCreatedISOString: new Date(orderTimeExpired).toISOString(),
+ validFor: swapValidFor
+ };
+ let random;
+
+ const data = {} as any;
+ data.gen = cloneableGenerator(shapeshiftOrderTimeRemaining)();
+
+ beforeAll(() => {
+ random = Math.random;
+ Math.random = () => 0.001;
+ });
+
+ afterAll(() => {
+ Math.random = random;
+ });
+
+ it('should call delay of one second', () => {
+ expect(data.gen.next(true).value).toEqual(call(delay, ONE_SECOND));
+ });
+
+ it('should select getSwap', () => {
+ expect(data.gen.next().value).toEqual(select(getSwap));
+ });
+
+ it('should handle if isValidUntil.isAfter(now)', () => {
+ data.clone2 = data.gen.clone();
+ const result = data.clone2.next(swapOrder).value;
+ expect(result).toHaveProperty('PUT');
+ expect(result.PUT.action.type).toEqual('SWAP_ORDER_TIME');
+ expect(result.PUT.action.payload).toBeGreaterThan(0);
+ });
+
+ it('should handle an no_deposits order state', () => {
+ const openOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'no_deposits' };
+ data.OPEN = data.gen.clone();
+ expect(data.OPEN.next(openOrder).value).toEqual(put(orderTimeSwap(0)));
+ expect(data.OPEN.next().value).toEqual(put(stopPollShapeshiftOrderStatus()));
+ expect(data.OPEN.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES }));
+ expect(data.OPEN.next().value).toEqual(
+ put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
+ );
+ });
+
+ it('should handle a failed order state', () => {
+ const cancOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'failed' };
+ data.CANC = data.gen.clone();
+ expect(data.CANC.next(cancOrder).value).toEqual(put(stopPollShapeshiftOrderStatus()));
+ expect(data.CANC.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES }));
+ expect(data.CANC.next().value).toEqual(
+ put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
+ );
+ });
+
+ it('should handle a received order state', () => {
+ const rcveOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'received' };
+ data.RCVE = data.gen.clone();
+ expect(data.RCVE.next(rcveOrder).value).toEqual(
+ put(showNotification('warning', ORDER_RECEIVED_MESSAGE, Infinity))
+ );
+ });
+
+ it('should handle a complete order state', () => {
+ const fillOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'complete' };
+ data.COMPLETE = data.gen.clone();
+ expect(data.COMPLETE.next(fillOrder).value).toEqual(put(stopPollShapeshiftOrderStatus()));
+ expect(data.COMPLETE.next().value).toEqual(
+ put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES })
+ );
+ expect(data.COMPLETE.next().value).toEqual(put(stopOrderTimerSwap()));
});
});
diff --git a/spec/sagas/swap/rates.spec.ts b/spec/sagas/swap/rates.spec.ts
index 7ef10769..5e8399c5 100644
--- a/spec/sagas/swap/rates.spec.ts
+++ b/spec/sagas/swap/rates.spec.ts
@@ -1,10 +1,20 @@
import { showNotification } from 'actions/notifications';
-import { loadBityRatesSucceededSwap } from 'actions/swap';
+import { loadBityRatesSucceededSwap, loadShapeshiftRatesSucceededSwap } from 'actions/swap';
import { getAllRates } from 'api/bity';
import { delay } from 'redux-saga';
-import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
+import { call, cancel, fork, put, race, take, takeLatest } from 'redux-saga/effects';
import { createMockTask } from 'redux-saga/utils';
-import { loadBityRates, handleBityRates, getBityRatesSaga } from 'sagas/swap/rates';
+import {
+ loadBityRates,
+ handleBityRates,
+ getBityRatesSaga,
+ loadShapeshiftRates,
+ getShapeShiftRatesSaga,
+ SHAPESHIFT_TIMEOUT,
+ handleShapeShiftRates
+} from 'sagas/swap/rates';
+import shapeshift from 'api/shapeshift';
+import { TypeKeys } from 'actions/swap/constants';
describe('loadBityRates*', () => {
const gen1 = loadBityRates();
@@ -51,6 +61,71 @@ describe('loadBityRates*', () => {
});
});
+describe('loadShapeshiftRates*', () => {
+ const gen1 = loadShapeshiftRates();
+ const gen2 = loadShapeshiftRates();
+
+ const apiResponse = {
+ ['1SSTANT']: {
+ id: '1STANT',
+ options: [
+ {
+ id: '1ST',
+ status: 'available',
+ image: 'https://shapeshift.io/images/coins/firstblood.png',
+ name: 'FirstBlood'
+ },
+ {
+ id: 'ANT',
+ status: 'available',
+ image: 'https://shapeshift.io/images/coins/aragon.png',
+ name: 'Aragon'
+ }
+ ],
+ rate: '0.24707537',
+ limit: 5908.29166225,
+ min: 7.86382979
+ }
+ };
+ let random;
+
+ beforeAll(() => {
+ random = Math.random;
+ Math.random = () => 0.001;
+ });
+
+ afterAll(() => {
+ Math.random = random;
+ });
+
+ it('should race shapeshift.getAllRates', () => {
+ expect(gen1.next().value).toEqual(
+ race({
+ tokens: call(shapeshift.getAllRates),
+ timeout: call(delay, SHAPESHIFT_TIMEOUT)
+ })
+ );
+ });
+
+ it('should put loadShapeshiftRatesSucceededSwap', () => {
+ expect(gen1.next({ tokens: apiResponse }).value).toEqual(
+ put(loadShapeshiftRatesSucceededSwap(apiResponse))
+ );
+ });
+
+ it('should call delay for 30 seconds', () => {
+ expect(gen1.next().value).toEqual(call(delay, 30000));
+ });
+
+ it('should handle an exception', () => {
+ const err = 'error';
+ gen2.next();
+ expect((gen2 as any).throw(err).value).toEqual(
+ put(showNotification('danger', `Error loading ShapeShift tokens - ${err}`))
+ );
+ });
+});
+
describe('handleBityRates*', () => {
const gen = handleBityRates();
const mockTask = createMockTask();
@@ -60,7 +135,7 @@ describe('handleBityRates*', () => {
});
it('should take SWAP_STOP_LOAD_BITY_RATES', () => {
- expect(gen.next(mockTask).value).toEqual(take('SWAP_STOP_LOAD_BITY_RATES'));
+ expect(gen.next(mockTask).value).toEqual(take(TypeKeys.SWAP_STOP_LOAD_BITY_RATES));
});
it('should cancel loadBityRatesTask', () => {
@@ -72,10 +147,43 @@ describe('handleBityRates*', () => {
});
});
+describe('handleShapeshiftRates*', () => {
+ const gen = handleShapeShiftRates();
+ const mockTask = createMockTask();
+
+ it('should fork loadShapeshiftRates', () => {
+ expect(gen.next().value).toEqual(fork(loadShapeshiftRates));
+ });
+
+ it('should take SWAP_STOP_LOAD_BITY_RATES', () => {
+ expect(gen.next(mockTask).value).toEqual(take(TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES));
+ });
+
+ it('should cancel loadShapeShiftRatesTask', () => {
+ expect(gen.next().value).toEqual(cancel(mockTask));
+ });
+
+ it('should be done', () => {
+ expect(gen.next().done).toEqual(true);
+ });
+});
+
describe('getBityRatesSaga*', () => {
const gen = getBityRatesSaga();
- it('should takeLatest SWAP_LOAD_RATES_REQUESTED', () => {
- expect(gen.next().value).toEqual(takeLatest('SWAP_LOAD_BITY_RATES_REQUESTED', handleBityRates));
+ it('should takeLatest SWAP_LOAD_BITY_RATES_REQUESTED', () => {
+ expect(gen.next().value).toEqual(
+ takeLatest(TypeKeys.SWAP_LOAD_BITY_RATES_REQUESTED, handleBityRates)
+ );
+ });
+});
+
+describe('getShapeshiftRatesSaga*', () => {
+ const gen = getShapeShiftRatesSaga();
+
+ it('should takeLatest SWAP_LOAD_BITY_RATES_REQUESTED', () => {
+ expect(gen.next().value).toEqual(
+ takeLatest(TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED, handleShapeShiftRates)
+ );
});
});
diff --git a/spec/sagas/transaction/network/gas.spec.ts b/spec/sagas/transaction/network/gas.spec.ts
index 80166c81..868369e5 100644
--- a/spec/sagas/transaction/network/gas.spec.ts
+++ b/spec/sagas/transaction/network/gas.spec.ts
@@ -107,7 +107,7 @@ describe('estimateGas*', () => {
});
it('should call delay', () => {
- expect(gens.gen.next(action).value).toEqual(call(delay, 1000));
+ expect(gens.gen.next(action).value).toEqual(call(delay, 250));
});
it('should select getNodeLib', () => {
diff --git a/spec/utils/helpers.spec.ts b/spec/utils/helpers.spec.ts
new file mode 100644
index 00000000..78b07ade
--- /dev/null
+++ b/spec/utils/helpers.spec.ts
@@ -0,0 +1,34 @@
+import { objectContainsObjectKeys } from 'utils/helpers';
+
+describe('objectContainsObjectKeys', () => {
+ it('should return true when object contains all keys of another object', () => {
+ const checkingObject = {
+ a: 1,
+ b: 2,
+ c: 3
+ };
+
+ const containingObject = {
+ a: 1,
+ b: 2,
+ c: 3,
+ d: 4
+ };
+
+ expect(objectContainsObjectKeys(checkingObject, containingObject)).toBeTruthy();
+ });
+
+ it('should return false when object does not contain all keys of another object', () => {
+ const checkingObject = {
+ a: 1,
+ b: 2,
+ c: 3
+ };
+
+ const containingObject = {
+ a: 1
+ };
+
+ expect(objectContainsObjectKeys(checkingObject, containingObject)).toBeFalsy();
+ });
+});