Shapeshift Integration (#564)
* progress * Normalize bity api response * Filter api response * Track swap information in component state * Update dropdown onchange * remove dead code * Update Min Max Validation * Update minmax err msg && fix onChangeOriginKind * Add origin & destination to redux state * Update types & Update tests * Update types * Update swap.spec.ts test * Remove commented out code * Remove hardcoded coin array * Create types.ts for swap reducer * Update swapinput type * Update bityRates in localStorage & Replace all instances of ...Kind / ...Amount props * Add shapeshift banner * initial work for sagas * Update Types * Update swap reducer initial state * Update Types & Store empty obj for bityRates / options * Update more types * added shapeshift file and rates comments * action reducers and prop mapping to components * add typings and swap icon * more actions reducers and sagas * debugging shapeshift service * add Headers * Fix content type * add order reset saga and ui fixes * remove console log and swap b/w Bity and Shapeshift * working state for Shapeshift and Bity - tested with mainnet * add icon component * UI improvements and fix select bug * fix timer bug * add bity fallback options and toFixed floats * tslint errors * add arrow to dropdown and add support footer * Add service provider * fix minor $ bug and stop timer on order complete * better load UX and dropdown UX * fixed single test * currRate prop bugs and reduce LS bloat * takeEvery on timer saga and don't clear state.options to restartSwap reducer * export tx sagas and fix minor type * Add ShapeShift Rates functionality when selecting a ShapeShift pair. * type fixes * BugFix: Don't change displayed ShapeShift Rate Inputs on every dropdown change Also contains some caching / performance improvements * BugFix: Don't remote rate inputs when falsy amount * fix type error * Progress commit * Implement saga logic * Make address field factory component * Shorten debounce time * Make new actions / sagas for handling single token lookup * Implement working version of litesend * Change saga into selector * Add failing spec * fix broken test * add debounce to error message * fix tests * update snapshots * test coverage * move setState disabled property from debounce so we instantly can go to next step on valid amounts * much deeper test coverage, fix debounce ux, and fix bity flashing at swap page load * fix minor failing test * seperate shapeshift erc20 token whitelist * fix saveState store bug * break orderTimeRemaining saga up and rewrite tests * add new swap icon * remove unused allowReadOnly prop * change offlineaware to walletdecrypt for litesend * fix LiteSend changewallet bug * fix error message UX * fix button styling to match develop * fix liteSend test * Fix LiteSend UX on unavl tokens, dropdown null value, and don't show decrypt in litesend after successful wallet decrypt. * add litesend network check
This commit is contained in:
parent
2f98555b00
commit
88532cdc3c
|
@ -27,6 +27,16 @@ export function loadBityRatesSucceededSwap(
|
|||
};
|
||||
}
|
||||
|
||||
export type TLoadShapeshiftSucceededSwap = typeof loadShapeshiftRatesSucceededSwap;
|
||||
export function loadShapeshiftRatesSucceededSwap(
|
||||
payload
|
||||
): interfaces.LoadShapshiftRatesSucceededSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TDestinationAddressSwap = typeof destinationAddressSwap;
|
||||
export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction {
|
||||
return {
|
||||
|
@ -49,6 +59,13 @@ export function loadBityRatesRequestedSwap(): interfaces.LoadBityRatesRequestedS
|
|||
};
|
||||
}
|
||||
|
||||
export type TLoadShapeshiftRequestedSwap = typeof loadShapeshiftRatesRequestedSwap;
|
||||
export function loadShapeshiftRatesRequestedSwap(): interfaces.LoadShapeshiftRequestedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED
|
||||
};
|
||||
}
|
||||
|
||||
export type TStopLoadBityRatesSwap = typeof stopLoadBityRatesSwap;
|
||||
export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction {
|
||||
return {
|
||||
|
@ -56,6 +73,13 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction
|
|||
};
|
||||
}
|
||||
|
||||
export type TStopLoadShapeshiftRatesSwap = typeof stopLoadShapeshiftRatesSwap;
|
||||
export function stopLoadShapeshiftRatesSwap(): interfaces.StopLoadShapeshiftRatesSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES
|
||||
};
|
||||
}
|
||||
|
||||
export type TOrderTimeSwap = typeof orderTimeSwap;
|
||||
export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction {
|
||||
return {
|
||||
|
@ -74,6 +98,16 @@ export function bityOrderCreateSucceededSwap(
|
|||
};
|
||||
}
|
||||
|
||||
export type TShapeshiftOrderCreateSucceededSwap = typeof shapeshiftOrderCreateSucceededSwap;
|
||||
export function shapeshiftOrderCreateSucceededSwap(
|
||||
payload: interfaces.ShapeshiftOrderResponse
|
||||
): interfaces.ShapeshiftOrderCreateSucceededSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TBityOrderCreateRequestedSwap = typeof bityOrderCreateRequestedSwap;
|
||||
export function bityOrderCreateRequestedSwap(
|
||||
amount: number,
|
||||
|
@ -82,7 +116,7 @@ export function bityOrderCreateRequestedSwap(
|
|||
mode: number = 0
|
||||
): interfaces.BityOrderCreateRequestedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED,
|
||||
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED,
|
||||
payload: {
|
||||
amount,
|
||||
destinationAddress,
|
||||
|
@ -92,29 +126,70 @@ export function bityOrderCreateRequestedSwap(
|
|||
};
|
||||
}
|
||||
|
||||
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
|
||||
export type TShapeshiftOrderCreateRequestedSwap = typeof shapeshiftOrderCreateRequestedSwap;
|
||||
export function shapeshiftOrderCreateRequestedSwap(
|
||||
withdrawal: string,
|
||||
originKind: string,
|
||||
destinationKind: string,
|
||||
destinationAmount: number
|
||||
): interfaces.ShapeshiftOrderCreateRequestedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_ORDER_CREATE_FAILED
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED,
|
||||
payload: {
|
||||
withdrawal,
|
||||
originKind,
|
||||
destinationKind,
|
||||
destinationAmount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type TOrderStatusSucceededSwap = typeof orderStatusSucceededSwap;
|
||||
export function orderStatusSucceededSwap(
|
||||
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED
|
||||
};
|
||||
}
|
||||
|
||||
export function shapeshiftOrderCreateFailedSwap(): interfaces.ShapeshiftOrderCreateFailedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED
|
||||
};
|
||||
}
|
||||
|
||||
export type TBityOrderStatusSucceededSwap = typeof bityOrderStatusSucceededSwap;
|
||||
export function bityOrderStatusSucceededSwap(
|
||||
payload: interfaces.BityOrderResponse
|
||||
): interfaces.OrderStatusSucceededSwapAction {
|
||||
): interfaces.BityOrderStatusSucceededSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TOrderStatusRequestedSwap = typeof orderStatusRequestedSwap;
|
||||
export function orderStatusRequestedSwap(): interfaces.OrderStatusRequestedSwapAction {
|
||||
export type TShapeshiftOrderStatusSucceededSwap = typeof shapeshiftOrderStatusSucceededSwap;
|
||||
export function shapeshiftOrderStatusSucceededSwap(
|
||||
payload: interfaces.ShapeshiftStatusResponse
|
||||
): interfaces.ShapeshiftOrderStatusSucceededSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TBityOrderStatusRequestedSwap = typeof bityOrderStatusRequested;
|
||||
export function bityOrderStatusRequested(): interfaces.BityOrderStatusRequestedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED
|
||||
};
|
||||
}
|
||||
|
||||
export type TShapeshiftOrderStatusRequestedSwap = typeof shapeshiftOrderStatusRequested;
|
||||
export function shapeshiftOrderStatusRequested(): interfaces.ShapeshiftOrderStatusRequestedSwapAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED
|
||||
};
|
||||
}
|
||||
|
||||
export type TStartOrderTimerSwap = typeof startOrderTimerSwap;
|
||||
export function startOrderTimerSwap(): interfaces.StartOrderTimerSwapAction {
|
||||
return {
|
||||
|
@ -136,9 +211,45 @@ export function startPollBityOrderStatus(): interfaces.StartPollBityOrderStatusA
|
|||
};
|
||||
}
|
||||
|
||||
export type TStartPollShapeshiftOrderStatus = typeof startPollShapeshiftOrderStatus;
|
||||
export function startPollShapeshiftOrderStatus(): interfaces.StartPollShapeshiftOrderStatusAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
export type TStopPollBityOrderStatus = typeof stopPollBityOrderStatus;
|
||||
export function stopPollBityOrderStatus(): interfaces.StopPollBityOrderStatusAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
export type TStopPollShapeshiftOrderStatus = typeof stopPollShapeshiftOrderStatus;
|
||||
export function stopPollShapeshiftOrderStatus(): interfaces.StopPollShapeshiftOrderStatusAction {
|
||||
return {
|
||||
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
export type TConfigureLiteSend = typeof configureLiteSend;
|
||||
export function configureLiteSend(): interfaces.ConfigureLiteSendAction {
|
||||
return { type: TypeKeys.SWAP_CONFIGURE_LITE_SEND };
|
||||
}
|
||||
|
||||
export type TShowLiteSend = typeof showLiteSend;
|
||||
export function showLiteSend(
|
||||
payload: interfaces.ShowLiteSendAction['payload']
|
||||
): interfaces.ShowLiteSendAction {
|
||||
return { type: TypeKeys.SWAP_SHOW_LITE_SEND, payload };
|
||||
}
|
||||
|
||||
export type TChangeSwapProvider = typeof changeSwapProvider;
|
||||
export function changeSwapProvider(
|
||||
payload: interfaces.ProviderName
|
||||
): interfaces.ChangeProviderSwapAcion {
|
||||
return {
|
||||
type: TypeKeys.SWAP_CHANGE_PROVIDER,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface Pairs {
|
|||
|
||||
export interface SwapInput {
|
||||
id: string;
|
||||
amount: number;
|
||||
amount: number | string;
|
||||
}
|
||||
|
||||
export interface SwapInputs {
|
||||
|
@ -24,6 +24,8 @@ export interface InitSwap {
|
|||
|
||||
export interface Option {
|
||||
id: string;
|
||||
status?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponseObj {
|
||||
|
@ -41,6 +43,11 @@ export interface LoadBityRatesSucceededSwapAction {
|
|||
payload: ApiResponse;
|
||||
}
|
||||
|
||||
export interface LoadShapshiftRatesSucceededSwapAction {
|
||||
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED;
|
||||
payload: ApiResponse;
|
||||
}
|
||||
|
||||
export interface DestinationAddressSwapAction {
|
||||
type: TypeKeys.SWAP_DESTINATION_ADDRESS;
|
||||
payload?: string;
|
||||
|
@ -55,6 +62,11 @@ export interface LoadBityRatesRequestedSwapAction {
|
|||
payload?: null;
|
||||
}
|
||||
|
||||
export interface LoadShapeshiftRequestedSwapAction {
|
||||
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED;
|
||||
payload?: null;
|
||||
}
|
||||
|
||||
export interface ChangeStepSwapAction {
|
||||
type: TypeKeys.SWAP_STEP;
|
||||
payload: number;
|
||||
|
@ -64,13 +76,17 @@ export interface StopLoadBityRatesSwapAction {
|
|||
type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES;
|
||||
}
|
||||
|
||||
export interface StopLoadShapeshiftRatesSwapAction {
|
||||
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES;
|
||||
}
|
||||
|
||||
export interface OrderSwapTimeSwapAction {
|
||||
type: TypeKeys.SWAP_ORDER_TIME;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export interface BityOrderCreateRequestedSwapAction {
|
||||
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED;
|
||||
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED;
|
||||
payload: {
|
||||
amount: number;
|
||||
destinationAddress: string;
|
||||
|
@ -79,6 +95,16 @@ export interface BityOrderCreateRequestedSwapAction {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ShapeshiftOrderCreateRequestedSwapAction {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED;
|
||||
payload: {
|
||||
withdrawal: string;
|
||||
originKind: string;
|
||||
destinationKind: string;
|
||||
destinationAmount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BityOrderInput {
|
||||
amount: string;
|
||||
currency: string;
|
||||
|
@ -99,6 +125,31 @@ export interface BityOrderResponse {
|
|||
status: string;
|
||||
}
|
||||
|
||||
export interface ShapeshiftOrderResponse {
|
||||
apiPubKey?: string;
|
||||
deposit: string;
|
||||
depositAmount: string;
|
||||
expiration: number;
|
||||
expirationFormatted?: string;
|
||||
inputCurrency?: string;
|
||||
maxLimit: number;
|
||||
minerFee: string;
|
||||
orderId: string;
|
||||
outputCurrency?: string;
|
||||
pair: string; // e.g. eth_bat
|
||||
provider?: ProviderName; // shapeshift
|
||||
quotedRate: string;
|
||||
withdrawal: string;
|
||||
withdrawalAmount: string;
|
||||
}
|
||||
|
||||
export interface ShapeshiftStatusResponse {
|
||||
status: string;
|
||||
address?: string;
|
||||
withdraw?: string;
|
||||
transaction: string;
|
||||
}
|
||||
|
||||
export type BityOrderPostResponse = BityOrderResponse & {
|
||||
payment_address: string;
|
||||
status: string;
|
||||
|
@ -109,23 +160,44 @@ export type BityOrderPostResponse = BityOrderResponse & {
|
|||
id: string;
|
||||
};
|
||||
|
||||
export type ProviderName = 'shapeshift' | 'bity';
|
||||
|
||||
export interface BityOrderCreateSucceededSwapAction {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED;
|
||||
payload: BityOrderPostResponse;
|
||||
}
|
||||
|
||||
export interface BityOrderCreateFailedSwapAction {
|
||||
type: TypeKeys.SWAP_ORDER_CREATE_FAILED;
|
||||
export interface ShapeshiftOrderCreateSucceededSwapAction {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED;
|
||||
payload: ShapeshiftOrderResponse;
|
||||
}
|
||||
export interface OrderStatusRequestedSwapAction {
|
||||
|
||||
export interface BityOrderCreateFailedSwapAction {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED;
|
||||
}
|
||||
|
||||
export interface ShapeshiftOrderCreateFailedSwapAction {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED;
|
||||
}
|
||||
|
||||
export interface BityOrderStatusRequestedSwapAction {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED;
|
||||
}
|
||||
|
||||
export interface OrderStatusSucceededSwapAction {
|
||||
export interface ShapeshiftOrderStatusRequestedSwapAction {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED;
|
||||
}
|
||||
|
||||
export interface BityOrderStatusSucceededSwapAction {
|
||||
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED;
|
||||
payload: BityOrderResponse;
|
||||
}
|
||||
|
||||
export interface ShapeshiftOrderStatusSucceededSwapAction {
|
||||
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED;
|
||||
payload: ShapeshiftStatusResponse;
|
||||
}
|
||||
|
||||
export interface StartOrderTimerSwapAction {
|
||||
type: TypeKeys.SWAP_ORDER_START_TIMER;
|
||||
}
|
||||
|
@ -138,22 +210,55 @@ export interface StartPollBityOrderStatusAction {
|
|||
type: TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS;
|
||||
}
|
||||
|
||||
export interface StartPollShapeshiftOrderStatusAction {
|
||||
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS;
|
||||
}
|
||||
|
||||
export interface StopPollBityOrderStatusAction {
|
||||
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS;
|
||||
}
|
||||
|
||||
export interface StopPollShapeshiftOrderStatusAction {
|
||||
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS;
|
||||
}
|
||||
|
||||
export interface ChangeProviderSwapAcion {
|
||||
type: TypeKeys.SWAP_CHANGE_PROVIDER;
|
||||
payload: ProviderName;
|
||||
}
|
||||
|
||||
export interface ConfigureLiteSendAction {
|
||||
type: TypeKeys.SWAP_CONFIGURE_LITE_SEND;
|
||||
}
|
||||
|
||||
export interface ShowLiteSendAction {
|
||||
type: TypeKeys.SWAP_SHOW_LITE_SEND;
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
/*** Action Type Union ***/
|
||||
export type SwapAction =
|
||||
| ChangeStepSwapAction
|
||||
| InitSwap
|
||||
| LoadBityRatesSucceededSwapAction
|
||||
| LoadShapshiftRatesSucceededSwapAction
|
||||
| DestinationAddressSwapAction
|
||||
| RestartSwapAction
|
||||
| LoadBityRatesRequestedSwapAction
|
||||
| LoadShapeshiftRequestedSwapAction
|
||||
| StopLoadBityRatesSwapAction
|
||||
| StopLoadShapeshiftRatesSwapAction
|
||||
| BityOrderCreateRequestedSwapAction
|
||||
| ShapeshiftOrderCreateRequestedSwapAction
|
||||
| BityOrderCreateSucceededSwapAction
|
||||
| OrderStatusSucceededSwapAction
|
||||
| ShapeshiftOrderCreateSucceededSwapAction
|
||||
| BityOrderStatusSucceededSwapAction
|
||||
| ShapeshiftOrderStatusSucceededSwapAction
|
||||
| StartPollBityOrderStatusAction
|
||||
| StartPollShapeshiftOrderStatusAction
|
||||
| BityOrderCreateFailedSwapAction
|
||||
| OrderSwapTimeSwapAction;
|
||||
| ShapeshiftOrderCreateFailedSwapAction
|
||||
| OrderSwapTimeSwapAction
|
||||
| ChangeProviderSwapAcion
|
||||
| ConfigureLiteSendAction
|
||||
| ShowLiteSendAction;
|
||||
|
|
|
@ -2,18 +2,31 @@ export enum TypeKeys {
|
|||
SWAP_STEP = 'SWAP_STEP',
|
||||
SWAP_INIT = 'SWAP_INIT',
|
||||
SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
|
||||
SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED = 'SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED',
|
||||
SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS',
|
||||
SWAP_RESTART = 'SWAP_RESTART',
|
||||
SWAP_LOAD_BITY_RATES_REQUESTED = 'SWAP_LOAD_BITY_RATES_REQUESTED',
|
||||
SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED = 'SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED',
|
||||
SWAP_STOP_LOAD_BITY_RATES = 'SWAP_STOP_LOAD_BITY_RATES',
|
||||
SWAP_STOP_LOAD_SHAPESHIFT_RATES = 'SWAP_STOP_LOAD_SHAPESHIFT_RATES',
|
||||
SWAP_ORDER_TIME = 'SWAP_ORDER_TIME',
|
||||
SWAP_BITY_ORDER_CREATE_SUCCEEDED = 'SWAP_BITY_ORDER_CREATE_SUCCEEDED',
|
||||
SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED',
|
||||
SWAP_BITY_ORDER_STATUS_SUCCEEDED = 'SWAP_BITY_ORDER_STATUS_SUCCEEDED',
|
||||
SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED',
|
||||
SWAP_BITY_ORDER_STATUS_REQUESTED = 'SWAP_BITY_ORDER_STATUS_REQUESTED',
|
||||
SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED',
|
||||
SWAP_ORDER_START_TIMER = 'SWAP_ORDER_START_TIMER',
|
||||
SWAP_ORDER_STOP_TIMER = 'SWAP_ORDER_STOP_TIMER',
|
||||
SWAP_START_POLL_BITY_ORDER_STATUS = 'SWAP_START_POLL_BITY_ORDER_STATUS',
|
||||
SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS',
|
||||
SWAP_STOP_POLL_BITY_ORDER_STATUS = 'SWAP_STOP_POLL_BITY_ORDER_STATUS',
|
||||
SWAP_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
|
||||
SWAP_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED'
|
||||
SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS',
|
||||
SWAP_BITY_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
|
||||
SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED',
|
||||
SWAP_BITY_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED',
|
||||
SWAP_SHAPESHIFT_ORDER_CREATE_FAILED = 'SWAP_SHAPESHIFT_ORDER_CREATE_FAILED',
|
||||
SWAP_CHANGE_PROVIDER = 'SWAP_CHANGE_PROVIDER',
|
||||
SWAP_CONFIGURE_LITE_SEND = 'SWAP_CONFIGURE_LITE_SEND',
|
||||
SWAP_SHOW_LITE_SEND = 'SWAP_SHOW_LITE_SEND'
|
||||
}
|
||||
|
|
|
@ -88,6 +88,34 @@ export function setTokenBalancesRejected(): types.SetTokenBalancesRejectedAction
|
|||
};
|
||||
}
|
||||
|
||||
export function setTokenBalancePending(
|
||||
payload: types.SetTokenBalancePendingAction['payload']
|
||||
): types.SetTokenBalancePendingAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetTokenBalanceFulfilled = typeof setTokenBalanceFulfilled;
|
||||
export function setTokenBalanceFulfilled(payload: {
|
||||
[key: string]: {
|
||||
balance: TokenValue;
|
||||
error: string | null;
|
||||
};
|
||||
}): types.SetTokenBalanceFulfilledAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function setTokenBalanceRejected(): types.SetTokenBalanceRejectedAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED
|
||||
};
|
||||
}
|
||||
|
||||
export type TScanWalletForTokens = typeof scanWalletForTokens;
|
||||
export function scanWalletForTokens(wallet: IWallet): types.ScanWalletForTokensAction {
|
||||
return {
|
||||
|
|
|
@ -63,6 +63,25 @@ export interface SetTokenBalancesRejectedAction {
|
|||
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED;
|
||||
}
|
||||
|
||||
export interface SetTokenBalancePendingAction {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING;
|
||||
payload: { tokenSymbol: string };
|
||||
}
|
||||
|
||||
export interface SetTokenBalanceFulfilledAction {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED;
|
||||
payload: {
|
||||
[key: string]: {
|
||||
balance: TokenValue;
|
||||
error: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetTokenBalanceRejectedAction {
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED;
|
||||
}
|
||||
|
||||
export interface ScanWalletForTokensAction {
|
||||
type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS;
|
||||
payload: IWallet;
|
||||
|
@ -108,6 +127,9 @@ export type WalletAction =
|
|||
| SetTokenBalancesPendingAction
|
||||
| SetTokenBalancesFulfilledAction
|
||||
| SetTokenBalancesRejectedAction
|
||||
| SetTokenBalancePendingAction
|
||||
| SetTokenBalanceFulfilledAction
|
||||
| SetTokenBalanceRejectedAction
|
||||
| ScanWalletForTokensAction
|
||||
| SetWalletTokensAction
|
||||
| SetWalletConfigAction;
|
||||
|
|
|
@ -10,6 +10,9 @@ export enum TypeKeys {
|
|||
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
|
||||
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
|
||||
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
|
||||
WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING',
|
||||
WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
|
||||
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
|
||||
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
|
||||
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
|
||||
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',
|
||||
|
|
|
@ -1,10 +1,34 @@
|
|||
import bityConfig, { WhitelistedCoins } from 'config/bity';
|
||||
import { checkHttpStatus, parseJSON, filter } from './utils';
|
||||
import bitcoinIcon from 'assets/images/bitcoin.png';
|
||||
import repIcon from 'assets/images/augur.png';
|
||||
import etherIcon from 'assets/images/ether.png';
|
||||
|
||||
const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => {
|
||||
return filter(from, arr) && filter(to, arr);
|
||||
};
|
||||
|
||||
const btcOptions = {
|
||||
id: 'BTC',
|
||||
status: 'available',
|
||||
image: bitcoinIcon,
|
||||
name: 'Bitcoin'
|
||||
};
|
||||
|
||||
const ethOptions = {
|
||||
id: 'ETH',
|
||||
status: 'available',
|
||||
image: etherIcon,
|
||||
name: 'Ether'
|
||||
};
|
||||
|
||||
const repOptions = {
|
||||
id: 'REP',
|
||||
status: 'available',
|
||||
image: repIcon,
|
||||
name: 'Augur'
|
||||
};
|
||||
|
||||
export function getAllRates() {
|
||||
const mappedRates = {};
|
||||
return _getAllRates().then(bityRates => {
|
||||
|
@ -14,9 +38,31 @@ export function getAllRates() {
|
|||
const to = { id: pairName.substring(3, 6) };
|
||||
// Check if rate exists= && check if the pair only crypto to crypto, not crypto to fiat, or any other combination
|
||||
if (parseFloat(each.rate_we_sell) && isCryptoPair(from.id, to.id, ['BTC', 'ETH', 'REP'])) {
|
||||
let fromOptions;
|
||||
let toOptions;
|
||||
switch (from.id) {
|
||||
case 'BTC':
|
||||
fromOptions = btcOptions;
|
||||
break;
|
||||
case 'ETH':
|
||||
fromOptions = ethOptions;
|
||||
break;
|
||||
case 'REP':
|
||||
fromOptions = repOptions;
|
||||
}
|
||||
switch (to.id) {
|
||||
case 'BTC':
|
||||
toOptions = btcOptions;
|
||||
break;
|
||||
case 'ETH':
|
||||
toOptions = ethOptions;
|
||||
break;
|
||||
case 'REP':
|
||||
toOptions = repOptions;
|
||||
}
|
||||
mappedRates[pairName] = {
|
||||
id: pairName,
|
||||
options: [from, to],
|
||||
options: [fromOptions, toOptions],
|
||||
rate: parseFloat(each.rate_we_sell)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import { checkHttpStatus, parseJSON } from 'api/utils';
|
||||
|
||||
const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
|
||||
|
||||
export const SHAPESHIFT_TOKEN_WHITELIST = [
|
||||
'OMG',
|
||||
'REP',
|
||||
'SNT',
|
||||
'SNGLS',
|
||||
'ZRX',
|
||||
'SWT',
|
||||
'ANT',
|
||||
'BAT',
|
||||
'BNT',
|
||||
'CVC',
|
||||
'DNT',
|
||||
'1ST',
|
||||
'GNO',
|
||||
'GNT',
|
||||
'EDG',
|
||||
'FUN',
|
||||
'RLC',
|
||||
'TRST',
|
||||
'GUP',
|
||||
'ETH'
|
||||
];
|
||||
export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETC', 'BTC'];
|
||||
|
||||
class ShapeshiftService {
|
||||
public whitelist = SHAPESHIFT_WHITELIST;
|
||||
private url = SHAPESHIFT_BASE_URL;
|
||||
private apiKey = '0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160';
|
||||
private postHeaders = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
public checkStatus(address) {
|
||||
return fetch(`${this.url}/txStat/${address}`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
public sendAmount(withdrawal, originKind, destinationKind, destinationAmount) {
|
||||
const pair = `${originKind.toLowerCase()}_${destinationKind.toLowerCase()}`;
|
||||
|
||||
return fetch(`${this.url}/sendamount`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
amount: destinationAmount,
|
||||
pair,
|
||||
apiKey: this.apiKey,
|
||||
withdrawal
|
||||
}),
|
||||
headers: new Headers(this.postHeaders)
|
||||
})
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
public getCoins() {
|
||||
return fetch(`${this.url}/getcoins`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
public getAllRates = async () => {
|
||||
const marketInfo = await this.getMarketInfo();
|
||||
const pairRates = await this.getPairRates(marketInfo);
|
||||
const checkAvl = await this.checkAvl(pairRates);
|
||||
const mappedRates = this.mapMarketInfo(checkAvl);
|
||||
return mappedRates;
|
||||
};
|
||||
|
||||
private getPairRates(marketInfo) {
|
||||
const filteredMarketInfo = marketInfo.filter(obj => {
|
||||
const { pair } = obj;
|
||||
const pairArr = pair.split('_');
|
||||
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1])
|
||||
? true
|
||||
: false;
|
||||
});
|
||||
const pairRates = filteredMarketInfo.map(p => {
|
||||
const { pair } = p;
|
||||
const singlePair = Promise.resolve(this.getSinglePairRate(pair));
|
||||
return { ...p, ...singlePair };
|
||||
});
|
||||
return pairRates;
|
||||
}
|
||||
|
||||
private async checkAvl(pairRates) {
|
||||
const avlCoins = await this.getAvlCoins();
|
||||
const mapAvl = pairRates.map(p => {
|
||||
const { pair } = p;
|
||||
const pairArr = pair.split('_');
|
||||
|
||||
if (pairArr[0] in avlCoins && pairArr[1] in avlCoins) {
|
||||
return {
|
||||
...p,
|
||||
...{
|
||||
[pairArr[0]]: {
|
||||
name: avlCoins[pairArr[0]].name,
|
||||
status: avlCoins[pairArr[0]].status,
|
||||
image: avlCoins[pairArr[0]].image
|
||||
},
|
||||
[pairArr[1]]: {
|
||||
name: avlCoins[pairArr[1]].name,
|
||||
status: avlCoins[pairArr[1]].status,
|
||||
image: avlCoins[pairArr[1]].image
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
return mapAvl;
|
||||
}
|
||||
|
||||
private getAvlCoins() {
|
||||
return fetch(`${this.url}/getcoins`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
private getSinglePairRate(pair) {
|
||||
return fetch(`${this.url}/rate/${pair}`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
private getMarketInfo() {
|
||||
return fetch(`${this.url}/marketinfo`)
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON);
|
||||
}
|
||||
|
||||
private isWhitelisted(coin) {
|
||||
return this.whitelist.includes(coin);
|
||||
}
|
||||
|
||||
private mapMarketInfo(marketInfo) {
|
||||
const tokenMap = {};
|
||||
marketInfo.forEach(m => {
|
||||
const originKind = m.pair.substring(0, 3);
|
||||
const destinationKind = m.pair.substring(4, 7);
|
||||
if (this.isWhitelisted(originKind) && this.isWhitelisted(destinationKind)) {
|
||||
const pairName = originKind + destinationKind;
|
||||
const { rate, limit, min } = m;
|
||||
tokenMap[pairName] = {
|
||||
id: pairName,
|
||||
options: [
|
||||
{
|
||||
id: originKind,
|
||||
status: m[originKind].status,
|
||||
image: m[originKind].image,
|
||||
name: m[originKind].name
|
||||
},
|
||||
{
|
||||
id: destinationKind,
|
||||
status: m[destinationKind].status,
|
||||
image: m[destinationKind].image,
|
||||
name: m[destinationKind].name
|
||||
}
|
||||
],
|
||||
rate,
|
||||
limit,
|
||||
min
|
||||
};
|
||||
}
|
||||
});
|
||||
return tokenMap;
|
||||
}
|
||||
}
|
||||
|
||||
const shapeshift = new ShapeshiftService();
|
||||
|
||||
export default shapeshift;
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1050 350" style="enable-background:new 0 0 1050 350;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#273C51;}
|
||||
.st2{fill:url(#SVGID_1_);}
|
||||
.st3{fill:#466284;}
|
||||
.st4{fill:#354D6A;}
|
||||
.st5{fill:url(#SVGID_2_);}
|
||||
.st6{fill:url(#SVGID_3_);}
|
||||
.st7{fill:url(#SVGID_4_);}
|
||||
.st8{fill:url(#SVGID_5_);}
|
||||
.st9{fill:url(#SVGID_6_);}
|
||||
.st10{fill:url(#SVGID_7_);}
|
||||
.st11{fill:url(#SVGID_8_);}
|
||||
.st12{fill:url(#SVGID_9_);}
|
||||
.st13{fill:url(#SVGID_10_);}
|
||||
.st14{fill:none;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M280.6,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
|
||||
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
|
||||
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
|
||||
C307.9,203.7,294,198.9,280.6,198.7z"/>
|
||||
<path class="st0" d="M353.8,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
|
||||
c0-16.6-8.6-26-24-26S354,247.5,354,263v44.9h-14V187.9h13.9V188.1z"/>
|
||||
<path class="st0" d="M504.4,308.2l-0.3-15.4c-6.7,11.7-19.5,17.1-31.2,17.1c-24.3,0-43.3-16.8-43.3-44.4
|
||||
c0-27.4,19.4-43.9,43.5-43.7c12.7,0,25.2,5.8,31.4,16.8l0.2-15.4h13.7v84.6h-13.5L504.4,308.2z M473.5,235.2
|
||||
c-16.8,0-30.3,12-30.3,30.8s13.5,31,30.3,31c40.8,0,40.8-62,0.2-62L473.5,235.2z"/>
|
||||
<path class="st0" d="M529.5,223.6h13.4l0.7,16.3c6.7-11.3,19.2-17.8,32.6-17.8c24.3,0.5,42.1,17.6,42.1,43.7
|
||||
c0,26.7-17.6,44-43,44c-12,0-25.4-5.1-32-17.1v55.2h-13.7V223.6z M604.2,266c0-19-12.5-30.3-29.8-30.3c-17.6,0-29.6,13-29.6,30.3
|
||||
s12.5,30.3,29.6,30.5C591.3,296.5,604.2,285.1,604.2,266z"/>
|
||||
<path class="st0" d="M708.9,294.3c-8.6,10.1-23.3,15.1-36.5,15.1c-26.2,0-44.5-17.3-44.5-44.2c0-25.5,18.3-43.9,43.9-43.9
|
||||
c25.9,0,45.6,15.9,42.3,49.7h-72c1.5,15.6,14.4,25.4,30.7,25.4c9.6,0,21.2-3.8,26.9-10.6l9.4,8.6H708.9z M700.6,259.4
|
||||
c-0.7-16.4-12-25.4-28.6-25.4c-14.7,0-27.6,8.9-30,25.2h58.6V259.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M771.2,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
|
||||
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
|
||||
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
|
||||
C798.5,203.7,784.6,198.9,771.2,198.7z"/>
|
||||
<path class="st0" d="M844.4,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
|
||||
c0-16.6-8.6-26-24-26s-26.7,12.2-26.7,27.6v44.9h-14V187.9h13.9V188.1z"/>
|
||||
<path class="st0" d="M920.6,307.8h14v-83.4h-14V307.8z M927.8,211.7l-11.1-12.2l11.1-12.2l11.1,12.2L927.8,211.7z"/>
|
||||
<g>
|
||||
<polygon class="st0" points="960.8,308 960.8,307.8 960.7,307.8 "/>
|
||||
<path class="st0" d="M974.7,217.6c0-12.7,5.8-18.3,14.9-18.3c0.2,0,0.4,0,0.5,0l3.2-12.1c-1.3-0.2-2.7-0.3-4-0.3
|
||||
c-17.8,0-28.4,11.3-28.4,30.7v6.7h-16.6v12.3h16.6v71.3h13.9v-71.3h16.8v-12.3h-16.8V217.6z"/>
|
||||
</g>
|
||||
<path class="st0" d="M1046.1,296.1c-1.1,0.2-2.2,0.3-3.3,0.3c-10.1,0-13.4-6.3-13.4-16.3v-43.6h18v-12.2h-17.9v-25.8l-14,1.5
|
||||
v24.2h-17v12.2h17v43.6c0,18.7,8.6,29.3,26.7,29c2.2-0.1,4.4-0.3,6.5-0.8L1046.1,296.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<polygon class="st1" points="216,82.1 230.5,-0.7 169.9,24.6 103.3,24.6 42.7,-0.8 57.3,82.1 43.6,128.3 56.5,136.4 0,186 0,232.6
|
||||
64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 146.3,266.7 146.3,266.7 146.3,266.7 188.7,153.8
|
||||
188.4,153.8 229.6,128.3 "/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="136.7364" y1="25.9921" x2="60.3198" y2="247.6647">
|
||||
<stop offset="0.1345" style="stop-color:#2B415B"/>
|
||||
<stop offset="0.3762" style="stop-color:#3B5676"/>
|
||||
<stop offset="0.6923" style="stop-color:#54769E"/>
|
||||
<stop offset="0.7901" style="stop-color:#52749B"/>
|
||||
<stop offset="0.8614" style="stop-color:#4D6C92"/>
|
||||
<stop offset="0.9244" style="stop-color:#436082"/>
|
||||
<stop offset="0.9822" style="stop-color:#364F6C"/>
|
||||
<stop offset="1" style="stop-color:#314863"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="97.7,100.3 0,186 136.1,264.3 136.6,102.9 "/>
|
||||
<polygon class="st3" points="83.8,153.3 136.2,293.4 136.6,161.1 "/>
|
||||
<polygon class="st4" points="188.7,153.8 136.2,293.4 136.6,161.1 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="230.1033" y1="127.4219" x2="34.0475" y2="14.229">
|
||||
<stop offset="0" style="stop-color:#54769E"/>
|
||||
<stop offset="0.4802" style="stop-color:#53749C"/>
|
||||
<stop offset="0.6878" style="stop-color:#4F6F95"/>
|
||||
<stop offset="0.8423" style="stop-color:#486588"/>
|
||||
<stop offset="0.9095" style="stop-color:#435F80"/>
|
||||
</linearGradient>
|
||||
<polygon class="st5" points="230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 42.7,-0.8 60.6,81.9 43.6,128.3 103.2,165.3
|
||||
136.3,201.3 136.3,201.6 136.5,201.4 136.7,201.6 136.7,201.3 169.8,165.3 229.6,128.3 212.6,82 "/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="342.5284" y1="63.8296" x2="150.4648" y2="63.8296">
|
||||
<stop offset="0.2539" style="stop-color:#20344C"/>
|
||||
<stop offset="0.4072" style="stop-color:#273D57"/>
|
||||
<stop offset="0.6733" style="stop-color:#395373"/>
|
||||
<stop offset="1" style="stop-color:#54769E"/>
|
||||
</linearGradient>
|
||||
<polygon class="st6" points="230.5,-0.7 216,82.1 229.6,128.3 212.6,82 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-74.3281" y1="63.7777" x2="124.2335" y2="63.7777">
|
||||
<stop offset="0.2539" style="stop-color:#54769E"/>
|
||||
<stop offset="0.4133" style="stop-color:#4D6E93"/>
|
||||
<stop offset="0.6897" style="stop-color:#3C5777"/>
|
||||
<stop offset="1" style="stop-color:#233850"/>
|
||||
</linearGradient>
|
||||
<polygon class="st7" points="42.7,-0.8 57.3,82.1 43.6,128.3 60.6,81.9 "/>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="138.4299" y1="-77.4169" x2="134.5027" y2="85.5632">
|
||||
<stop offset="6.545247e-03" style="stop-color:#54769E"/>
|
||||
<stop offset="0.1993" style="stop-color:#507198"/>
|
||||
<stop offset="0.4502" style="stop-color:#466488"/>
|
||||
<stop offset="0.7318" style="stop-color:#354F6D"/>
|
||||
<stop offset="1" style="stop-color:#21354D"/>
|
||||
</linearGradient>
|
||||
<polygon class="st8" points="42.7,-0.8 103.3,24.6 169.9,24.6 230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 "/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="173.2798" y1="-23.2345" x2="12.7505" y2="132.5687">
|
||||
<stop offset="0.2539" style="stop-color:#54769E"/>
|
||||
<stop offset="0.4102" style="stop-color:#4D6E93"/>
|
||||
<stop offset="0.6813" style="stop-color:#3C5777"/>
|
||||
<stop offset="1" style="stop-color:#22364E"/>
|
||||
</linearGradient>
|
||||
<polygon class="st9" points="60.6,81.9 57.6,90.2 120.5,32.2 "/>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="114.997" y1="-2.4443" x2="248.7759" y2="116.8474">
|
||||
<stop offset="0.2539" style="stop-color:#54769E"/>
|
||||
<stop offset="0.4102" style="stop-color:#4D6E93"/>
|
||||
<stop offset="0.6813" style="stop-color:#3C5777"/>
|
||||
<stop offset="1" style="stop-color:#22364E"/>
|
||||
</linearGradient>
|
||||
<polygon class="st10" points="212.6,82 153,32.2 215.5,89.8 "/>
|
||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-31.9333" y1="230.8414" x2="255.118" y2="333.2895">
|
||||
<stop offset="0.2664" style="stop-color:#54769E"/>
|
||||
<stop offset="1" style="stop-color:#425E7F"/>
|
||||
</linearGradient>
|
||||
<polygon class="st11" points="0,186 146.3,266.7 164.8,313 125.6,327.9 64.6,321.6 0,232.6 "/>
|
||||
<polygon class="st0" points="121.1,252.8 64.8,321.4 64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 "/>
|
||||
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="97.761" y1="-67.9411" x2="268.6103" y2="84.2801">
|
||||
<stop offset="0.4609" style="stop-color:#54769E;stop-opacity:0"/>
|
||||
<stop offset="0.5699" style="stop-color:#52739A;stop-opacity:0.2156"/>
|
||||
<stop offset="0.6764" style="stop-color:#4A698E;stop-opacity:0.4266"/>
|
||||
<stop offset="0.782" style="stop-color:#3D597B;stop-opacity:0.6356"/>
|
||||
<stop offset="0.8863" style="stop-color:#2C435F;stop-opacity:0.8422"/>
|
||||
<stop offset="0.9661" style="stop-color:#1B2E45"/>
|
||||
</linearGradient>
|
||||
<polygon class="st12" points="212.6,82 230.5,-0.7 178.4,26.7 153,32.2 "/>
|
||||
<polygon class="st0" points="136.6,201.6 120.1,183.5 136.6,165.3 153.2,183.5 "/>
|
||||
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="136.6099" y1="347.9733" x2="136.6099" y2="-96.2296">
|
||||
<stop offset="0.2539" style="stop-color:#54769E"/>
|
||||
<stop offset="0.4102" style="stop-color:#4D6E93"/>
|
||||
<stop offset="0.6813" style="stop-color:#3C5777"/>
|
||||
<stop offset="1" style="stop-color:#22364E"/>
|
||||
</linearGradient>
|
||||
<polygon class="st13" points="135,35.4 136.7,35.8 138.2,35.5 136.6,141 "/>
|
||||
<path class="st14" d="M77.6,35.5"/>
|
||||
<path class="st14" d="M75.1,35.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.0 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.8 KiB |
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="620px" height="620px" viewBox="0 0 620 620" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>swap</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<circle id="path-1" cx="170" cy="170" r="170"></circle>
|
||||
<circle id="path-3" cx="170" cy="170" r="170"></circle>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="swap">
|
||||
<g id="Yellow-Coin" transform="translate(0.000000, 280.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
|
||||
<rect id="Rectangle" fill="#FFE14D" mask="url(#mask-2)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
|
||||
<rect id="Rectangle" fill="#FFCC33" mask="url(#mask-2)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
|
||||
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="160"></circle>
|
||||
<circle id="Oval-3" stroke="#F28618" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="96.3333333"></circle>
|
||||
</g>
|
||||
<g id="Blue-Coin" transform="translate(280.000000, 0.000000)">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
|
||||
<rect id="Rectangle" fill="#6EA6E8" mask="url(#mask-4)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
|
||||
<rect id="Rectangle" fill="#5C9BE4" mask="url(#mask-4)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
|
||||
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="160"></circle>
|
||||
<circle id="Oval-3" stroke="#2F79CF" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="96.3333333"></circle>
|
||||
</g>
|
||||
<g id="Group" transform="translate(226.000000, 320.000000)" fill-rule="nonzero">
|
||||
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
|
||||
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
|
||||
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(197.000000, 174.500000) rotate(180.000000) translate(-197.000000, -174.500000) translate(0.000000, 49.000000)" fill-rule="nonzero">
|
||||
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
|
||||
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
|
||||
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import { AddressFieldFactory } from './AddressFieldFactory';
|
||||
import { donationAddressMap } from 'config/data';
|
||||
|
||||
export const AddressField: React.SFC<{}> = () => (
|
||||
<AddressFieldFactory
|
||||
withProps={({ currentTo, isValid, onChange, readOnly }) => (
|
||||
<input
|
||||
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
|
||||
type="text"
|
||||
value={currentTo.raw}
|
||||
placeholder={donationAddressMap.ETH}
|
||||
readOnly={!!readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export * from './AddressField';
|
|
@ -1,8 +1,9 @@
|
|||
import { Query } from 'components/renderCbs';
|
||||
import { setCurrentTo, TSetCurrentTo } from 'actions/transaction';
|
||||
import { AddressInput } from './AddressInput';
|
||||
import { AddressInputFactory } from './AddressInputFactory';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ICurrentTo } from 'selectors/transaction';
|
||||
|
||||
interface DispatchProps {
|
||||
setCurrentTo: TSetCurrentTo;
|
||||
|
@ -10,12 +11,20 @@ interface DispatchProps {
|
|||
|
||||
interface OwnProps {
|
||||
to: string | null;
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
}
|
||||
|
||||
export interface CallbackProps {
|
||||
isValid: boolean;
|
||||
readOnly: boolean;
|
||||
currentTo: ICurrentTo;
|
||||
onChange(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
type Props = DispatchProps & DispatchProps & OwnProps;
|
||||
|
||||
//TODO: add ens resolving
|
||||
class AddressFieldClass extends React.Component<Props, {}> {
|
||||
class AddressFieldFactoryClass extends React.Component<Props, {}> {
|
||||
public componentDidMount() {
|
||||
// this 'to' parameter can be either token or actual field related
|
||||
const { to } = this.props;
|
||||
|
@ -25,7 +34,7 @@ class AddressFieldClass extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
return <AddressInput onChange={this.setAddress} />;
|
||||
return <AddressInputFactory onChange={this.setAddress} withProps={this.props.withProps} />;
|
||||
}
|
||||
|
||||
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
|
@ -34,10 +43,14 @@ class AddressFieldClass extends React.Component<Props, {}> {
|
|||
};
|
||||
}
|
||||
|
||||
const AddressField = connect(null, { setCurrentTo })(AddressFieldClass);
|
||||
const AddressField = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
|
||||
|
||||
const DefaultAddressField: React.SFC<{}> = () => (
|
||||
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} />} />
|
||||
interface DefaultAddressFieldProps {
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
}
|
||||
|
||||
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ withProps }) => (
|
||||
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} withProps={withProps} />} />
|
||||
);
|
||||
|
||||
export { DefaultAddressField as AddressField };
|
||||
export { DefaultAddressField as AddressFieldFactory };
|
|
@ -3,10 +3,10 @@ import { Identicon } from 'components/ui';
|
|||
import translate from 'translations';
|
||||
//import { EnsAddress } from './components';
|
||||
import { Query } from 'components/renderCbs';
|
||||
import { donationAddressMap } from 'config/data';
|
||||
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { CallbackProps } from 'components/AddressFieldFactory';
|
||||
|
||||
interface StateProps {
|
||||
currentTo: ICurrentTo;
|
||||
|
@ -14,14 +14,15 @@ interface StateProps {
|
|||
}
|
||||
interface OwnProps {
|
||||
onChange(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
|
||||
//TODO: ENS handling
|
||||
class AddressInputClass extends Component<Props> {
|
||||
class AddressInputFactoryClass extends Component<Props> {
|
||||
public render() {
|
||||
const { currentTo, onChange, isValid } = this.props;
|
||||
const { currentTo, onChange, isValid, withProps } = this.props;
|
||||
const { raw } = currentTo;
|
||||
return (
|
||||
<div className="row form-group">
|
||||
|
@ -29,16 +30,9 @@ class AddressInputClass extends Component<Props> {
|
|||
<label>{translate('SEND_addr')}:</label>
|
||||
<Query
|
||||
params={['readOnly']}
|
||||
withQuery={({ readOnly }) => (
|
||||
<input
|
||||
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
|
||||
type="text"
|
||||
value={raw}
|
||||
placeholder={donationAddressMap.ETH}
|
||||
readOnly={!!readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
withQuery={({ readOnly }) =>
|
||||
withProps({ currentTo, isValid, onChange, readOnly: !!readOnly })
|
||||
}
|
||||
/>
|
||||
{/*<EnsAddress ensAddress={ensAddress} />*/}
|
||||
</div>
|
||||
|
@ -50,7 +44,7 @@ class AddressInputClass extends Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export const AddressInput = connect((state: AppState) => ({
|
||||
export const AddressInputFactory = connect((state: AppState) => ({
|
||||
currentTo: getCurrentTo(state),
|
||||
isValid: isValidCurrentTo(state)
|
||||
}))(AddressInputClass);
|
||||
}))(AddressInputFactoryClass);
|
|
@ -0,0 +1 @@
|
|||
export * from './AddressFieldFactory';
|
|
@ -44,3 +44,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#NAV_Swap a:before {
|
||||
content:"";
|
||||
display: inline-block;
|
||||
margin-top: -.1rem;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
background-image: url('~assets/images/swap.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,11 @@ class NavigationLink extends React.Component<Props, {}> {
|
|||
</Link>
|
||||
);
|
||||
|
||||
return <li className="NavigationLink">{linkEl}</li>;
|
||||
return (
|
||||
<li id={link.name} className="NavigationLink">
|
||||
{linkEl}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.SwapDropdown {
|
||||
position: relative;
|
||||
button {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 2px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:active, &:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
> li {
|
||||
margin: 0;
|
||||
&:first-child {
|
||||
padding-top: 4px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
> a {
|
||||
font-weight: 300;
|
||||
&.active {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SwapDropdown-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
min-width: 500px;
|
||||
left: 50%;
|
||||
|
||||
top: 50px;
|
||||
transform: translateX(-50%);
|
||||
list-style: none;
|
||||
font-size: 0.8rem;
|
||||
text-align: left;
|
||||
z-index: 500;
|
||||
background: white;
|
||||
box-shadow: 2px 1px 60px rgba(0,0,0,.175);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-right: 10px solid transparent;
|
||||
border-left: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #fff;
|
||||
}
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
width: 33.3%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
li > a {
|
||||
display: block;
|
||||
clear: both;
|
||||
padding: 5px 20px;
|
||||
color: #163151;
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
background-color: #163151;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.inactive {
|
||||
a {
|
||||
color: grey;
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color:#163151;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
img {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
strong {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.SwapDropdown-desc {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.SwapDropdown-item {
|
||||
position: relative;
|
||||
img {
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import React, { Component } from 'react';
|
||||
import './SwapDropdown.scss';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export interface SingleCoin {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
ariaLabel: string;
|
||||
options: SingleCoin[];
|
||||
value: string;
|
||||
onChange(value: T): void;
|
||||
}
|
||||
|
||||
class SwapDropdown<T> extends Component<Props<T>, {}> {
|
||||
public state = {
|
||||
open: false
|
||||
};
|
||||
|
||||
private dropdown: HTMLElement | null;
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('click', this.clickHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('click', this.clickHandler);
|
||||
}
|
||||
|
||||
public handleClickOutside() {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { open } = this.state;
|
||||
const { options, value } = this.props;
|
||||
const dropdownGrid = classnames(open && 'open', 'SwapDropdown-grid');
|
||||
|
||||
const mappedCoins = options.sort((a, b) => (a.id > b.id ? 1 : -1)).map((coin: SingleCoin) => {
|
||||
const cn = classnames(coin.status !== 'available' && 'inactive', 'SwapDropdown-item');
|
||||
return (
|
||||
<li className={cn} key={coin.id}>
|
||||
<a onClick={coin.status === 'available' ? this.onChange.bind(null, coin.id) : null}>
|
||||
<img src={coin.image} height="20" width="20" />
|
||||
{/* <div className="SwapDropdown-desc"> */}
|
||||
<strong>{coin.id}</strong>
|
||||
<br />
|
||||
<small>{coin.name}</small>
|
||||
{/* </div> */}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="SwapDropdown" ref={el => (this.dropdown = el)}>
|
||||
<button onClick={this.toggleDropdown}>
|
||||
{value}
|
||||
<i className="caret" />
|
||||
</button>
|
||||
<ul className={dropdownGrid}>{mappedCoins}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private toggleDropdown = () => {
|
||||
this.setState({
|
||||
open: !this.state.open
|
||||
});
|
||||
};
|
||||
|
||||
private onChange = (value: any) => {
|
||||
this.props.onChange(value);
|
||||
if (this.state.open) {
|
||||
this.setState({
|
||||
open: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private clickHandler = (ev: Event) => {
|
||||
if (!this.state.open || !this.dropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.dropdown !== ev.target &&
|
||||
ev.target instanceof HTMLElement &&
|
||||
!this.dropdown.contains(ev.target)
|
||||
) {
|
||||
this.setState({
|
||||
open: false
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SwapDropdown;
|
|
@ -8,6 +8,7 @@ export { default as QRCode } from './QRCode';
|
|||
export { default as NewTabLink } from './NewTabLink';
|
||||
export { default as UnitDisplay } from './UnitDisplay';
|
||||
export { default as Spinner } from './Spinner';
|
||||
export { default as SwapDropdown } from './SwapDropdown';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export * from './ConditionalInput';
|
||||
export * from './Aux';
|
||||
|
|
|
@ -46,6 +46,8 @@ export const MINIMUM_PASSWORD_LENGTH = 9;
|
|||
|
||||
export const knowledgeBaseURL = 'https://myetherwallet.github.io/knowledge-base';
|
||||
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
|
||||
// Note: add the real referral url once you know it
|
||||
export const shapeshiftReferralURL = 'https://shapeshift.io';
|
||||
export const ledgerReferralURL = 'https://www.ledgerwallet.com/r/fa4b?path=/products/';
|
||||
export const trezorReferralURL = 'https://trezor.io/?a=myetherwallet.com';
|
||||
export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mew';
|
||||
|
|
|
@ -54,3 +54,6 @@
|
|||
margin-top: $space * 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
import { TChangeStepSwap, TInitSwap } from 'actions/swap';
|
||||
import { NormalizedBityRates, NormalizedOptions, SwapInput } from 'reducers/swap/types';
|
||||
import { TChangeStepSwap, TInitSwap, TChangeSwapProvider, ProviderName } from 'actions/swap';
|
||||
import {
|
||||
NormalizedBityRates,
|
||||
NormalizedShapeshiftRates,
|
||||
NormalizedOptions,
|
||||
SwapInput
|
||||
} from 'reducers/swap/types';
|
||||
import SimpleButton from 'components/ui/SimpleButton';
|
||||
import bityConfig, { generateKindMax, generateKindMin, WhitelistedCoins } from 'config/bity';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { combineAndUpper } from 'utils/formatters';
|
||||
import { Dropdown } from 'components/ui';
|
||||
import { SwapDropdown } from 'components/ui';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import intersection from 'lodash/intersection';
|
||||
import without from 'lodash/without';
|
||||
import { merge, reject, debounce } from 'lodash';
|
||||
import './CurrencySwap.scss';
|
||||
|
||||
export interface StateProps {
|
||||
bityRates: NormalizedBityRates;
|
||||
shapeshiftRates: NormalizedShapeshiftRates;
|
||||
provider: ProviderName;
|
||||
options: NormalizedOptions;
|
||||
}
|
||||
|
||||
export interface ActionProps {
|
||||
changeStepSwap: TChangeStepSwap;
|
||||
initSwap: TInitSwap;
|
||||
swapProvider: TChangeSwapProvider;
|
||||
}
|
||||
|
||||
interface State {
|
||||
disabled: boolean;
|
||||
origin: SwapInput;
|
||||
destination: SwapInput;
|
||||
originKindOptions: WhitelistedCoins[];
|
||||
destinationKindOptions: WhitelistedCoins[];
|
||||
originKindOptions: any[];
|
||||
destinationKindOptions: any[];
|
||||
originErr: string;
|
||||
destinationErr: string;
|
||||
timeout: boolean;
|
||||
}
|
||||
|
||||
type Props = StateProps & ActionProps;
|
||||
|
@ -36,26 +44,63 @@ type Props = StateProps & ActionProps;
|
|||
export default class CurrencySwap extends Component<Props, State> {
|
||||
public state = {
|
||||
disabled: true,
|
||||
origin: { id: 'BTC', amount: NaN } as SwapInput,
|
||||
destination: { id: 'ETH', amount: NaN } as SwapInput,
|
||||
originKindOptions: ['BTC', 'ETH'] as WhitelistedCoins[],
|
||||
destinationKindOptions: ['ETH'] as WhitelistedCoins[],
|
||||
origin: {
|
||||
id: 'BTC',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/bitcoin.png',
|
||||
amount: NaN
|
||||
} as SwapInput,
|
||||
destination: {
|
||||
id: 'ETH',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/ether.png',
|
||||
amount: NaN
|
||||
} as SwapInput,
|
||||
originKindOptions: [],
|
||||
destinationKindOptions: [],
|
||||
originErr: '',
|
||||
destinationErr: ''
|
||||
destinationErr: '',
|
||||
timeout: false
|
||||
};
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { origin, destination } = this.state;
|
||||
public debouncedCreateErrString = debounce((origin, destination, showError) => {
|
||||
const createErrString = (
|
||||
originKind: WhitelistedCoins,
|
||||
amount: number,
|
||||
destKind: WhitelistedCoins
|
||||
) => {
|
||||
const rate = this.getMinMax(originKind, destKind);
|
||||
let errString;
|
||||
if (amount > rate.max) {
|
||||
errString = `Maximum ${rate.max} ${originKind}`;
|
||||
} else {
|
||||
errString = `Minimum ${rate.min} ${originKind}`;
|
||||
}
|
||||
return errString;
|
||||
};
|
||||
const originErr = showError ? createErrString(origin.id, origin.amount, destination.id) : '';
|
||||
const destinationErr = showError
|
||||
? createErrString(destination.id, destination.amount, origin.id)
|
||||
: '';
|
||||
this.setErrorMessages(originErr, destinationErr);
|
||||
}, 1000);
|
||||
|
||||
public componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
timeout: true
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const { origin } = this.state;
|
||||
const { options } = this.props;
|
||||
if (origin !== prevState.origin) {
|
||||
this.setDisabled(origin, destination);
|
||||
}
|
||||
if (options.allIds !== prevProps.options.allIds) {
|
||||
const originKindOptions: WhitelistedCoins[] = intersection<any>(
|
||||
options.allIds,
|
||||
this.state.originKindOptions
|
||||
|
||||
if (options.allIds && options.byId) {
|
||||
const originKindOptions: any[] = Object.values(options.byId);
|
||||
const destinationKindOptions: any[] = Object.values(
|
||||
reject<any>(options.byId, o => o.id === origin.id)
|
||||
);
|
||||
const destinationKindOptions: WhitelistedCoins[] = without<any>(options.allIds, origin.id);
|
||||
|
||||
this.setState({
|
||||
originKindOptions,
|
||||
destinationKindOptions
|
||||
|
@ -63,54 +108,110 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public getMinMax = (kind: WhitelistedCoins) => {
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { origin, destination } = this.state;
|
||||
const { options, bityRates, shapeshiftRates } = this.props;
|
||||
if (origin !== prevState.origin) {
|
||||
this.setDisabled(origin, destination);
|
||||
}
|
||||
|
||||
const originCap = origin.id.toUpperCase();
|
||||
const destCap = destination.id.toUpperCase();
|
||||
const { provider } = this.props;
|
||||
|
||||
const ensureCorrectProvider =
|
||||
(originCap === 'BTC' && destCap === 'ETH') || (destCap === 'BTC' && originCap === 'ETH');
|
||||
const ensureBityRatesLoaded =
|
||||
bityRates.allIds.includes('ETHBTC') && bityRates.allIds.includes('BTCETH');
|
||||
const ensureShapeshiftRatesLoaded = shapeshiftRates.allIds.length > 0;
|
||||
|
||||
if (ensureBityRatesLoaded && ensureCorrectProvider) {
|
||||
if (provider === 'shapeshift') {
|
||||
this.props.swapProvider('bity');
|
||||
}
|
||||
} else if (ensureShapeshiftRatesLoaded) {
|
||||
if (provider !== 'shapeshift') {
|
||||
this.props.swapProvider('shapeshift');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.allIds !== prevProps.options.allIds && options.byId) {
|
||||
const originKindOptions: any[] = Object.values(options.byId);
|
||||
const destinationKindOptions: any[] = Object.values(
|
||||
reject<any>(options.byId, o => o.id === origin.id)
|
||||
);
|
||||
|
||||
this.setState({
|
||||
originKindOptions,
|
||||
destinationKindOptions
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public rateMixer = () => {
|
||||
const { shapeshiftRates, bityRates } = this.props;
|
||||
return merge(shapeshiftRates, bityRates);
|
||||
};
|
||||
|
||||
public getMinMax = (originKind: WhitelistedCoins, destinationKind) => {
|
||||
let min;
|
||||
let max;
|
||||
if (kind !== 'BTC') {
|
||||
const bityPairRate = this.props.bityRates.byId['BTC' + kind].rate;
|
||||
min = generateKindMin(bityPairRate, kind);
|
||||
max = generateKindMax(bityPairRate, kind);
|
||||
|
||||
const { provider, bityRates } = this.props;
|
||||
|
||||
if (provider === 'bity' && bityRates.allIds.length > 2) {
|
||||
if (originKind !== 'BTC') {
|
||||
const pairRate = this.rateMixer().byId['BTC' + originKind].rate;
|
||||
min = generateKindMin(pairRate, originKind);
|
||||
max = generateKindMax(pairRate, originKind);
|
||||
} else {
|
||||
min = bityConfig.BTCMin;
|
||||
max = bityConfig.BTCMax;
|
||||
}
|
||||
} else {
|
||||
min = bityConfig.BTCMin;
|
||||
max = bityConfig.BTCMax;
|
||||
const pair = (this.rateMixer() as NormalizedShapeshiftRates).byId[
|
||||
originKind + destinationKind
|
||||
];
|
||||
min = pair.min;
|
||||
max = pair.limit;
|
||||
}
|
||||
return { min, max };
|
||||
};
|
||||
|
||||
public isMinMaxValid = (amount: number, kind: WhitelistedCoins) => {
|
||||
const rate = this.getMinMax(kind);
|
||||
const higherThanMin = amount >= rate.min;
|
||||
const lowerThanMax = amount <= rate.max;
|
||||
public isMinMaxValid = (originAmount: number, originKind: WhitelistedCoins, destinationKind) => {
|
||||
const rate = this.getMinMax(originKind, destinationKind);
|
||||
const higherThanMin = originAmount >= rate.min;
|
||||
const lowerThanMax = originAmount <= rate.max;
|
||||
return higherThanMin && lowerThanMax;
|
||||
};
|
||||
|
||||
public setDisabled(origin: SwapInput, destination: SwapInput) {
|
||||
this.clearErrMessages();
|
||||
const amountsValid = origin.amount && destination.amount;
|
||||
const minMaxValid = this.isMinMaxValid(origin.amount, origin.id);
|
||||
|
||||
const minMaxValid = this.isMinMaxValid(origin.amount as number, origin.id, destination.id);
|
||||
const disabled = !(amountsValid && minMaxValid);
|
||||
|
||||
const createErrString = (kind: WhitelistedCoins, amount: number) => {
|
||||
const rate = this.getMinMax(kind);
|
||||
let errString;
|
||||
if (amount > rate.max) {
|
||||
errString = `Maximum ${rate.max} ${kind}`;
|
||||
} else {
|
||||
errString = `Minimum ${rate.min} ${kind}`;
|
||||
}
|
||||
return errString;
|
||||
};
|
||||
|
||||
const showError = disabled && amountsValid;
|
||||
const originErr = showError ? createErrString(origin.id, origin.amount) : '';
|
||||
const destinationErr = showError ? createErrString(destination.id, destination.amount) : '';
|
||||
|
||||
this.setState({
|
||||
disabled,
|
||||
disabled
|
||||
});
|
||||
|
||||
this.debouncedCreateErrString(origin, destination, showError);
|
||||
}
|
||||
|
||||
public setErrorMessages = (originErr, destinationErr) => {
|
||||
this.setState({
|
||||
originErr,
|
||||
destinationErr
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public clearErrMessages = () => {
|
||||
this.setState({
|
||||
originErr: '',
|
||||
destinationErr: ''
|
||||
});
|
||||
};
|
||||
|
||||
public onClickStartSwap = () => {
|
||||
const { origin, destination } = this.state;
|
||||
|
@ -129,8 +230,8 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
|
||||
if (amount || amount === 0) {
|
||||
const pairName = combineAndUpper(origin.id, destination.id);
|
||||
const bityRate = this.props.bityRates.byId[pairName].rate;
|
||||
const destinationAmount = amount * bityRate;
|
||||
const rate = this.rateMixer().byId[pairName].rate;
|
||||
const destinationAmount = amount * rate;
|
||||
this.setState({
|
||||
origin: { ...this.state.origin, amount },
|
||||
destination: { ...this.state.destination, amount: destinationAmount }
|
||||
|
@ -143,8 +244,8 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
|
||||
if (amount || amount === 0) {
|
||||
const pairNameReversed = combineAndUpper(destination.id, origin.id);
|
||||
const bityRate = this.props.bityRates.byId[pairNameReversed].rate;
|
||||
const originAmount = amount * bityRate;
|
||||
const rate = this.rateMixer().byId[pairNameReversed].rate;
|
||||
const originAmount = amount * rate;
|
||||
this.setState({
|
||||
origin: { ...this.state.origin, amount: originAmount },
|
||||
destination: {
|
||||
|
@ -168,69 +269,80 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
|
||||
public onChangeOriginKind = (newOption: WhitelistedCoins) => {
|
||||
const { origin, destination, destinationKindOptions } = this.state;
|
||||
const newDestinationAmount = () => {
|
||||
const pairName = combineAndUpper(destination.id, origin.id);
|
||||
const bityRate = this.props.bityRates.byId[pairName].rate;
|
||||
return bityRate * origin.amount;
|
||||
const { options, initSwap } = this.props;
|
||||
|
||||
const newOrigin = { ...origin, id: newOption, amount: '' };
|
||||
const newDest = {
|
||||
id: newOption === destination.id ? origin.id : destination.id,
|
||||
amount: ''
|
||||
};
|
||||
this.setState({
|
||||
origin: { ...origin, id: newOption },
|
||||
destination: {
|
||||
id: newOption === destination.id ? origin.id : destination.id,
|
||||
amount: newDestinationAmount() ? newDestinationAmount() : destination.amount
|
||||
},
|
||||
destinationKindOptions: without([...destinationKindOptions, origin.id], newOption)
|
||||
origin: newOrigin,
|
||||
destination: newDest,
|
||||
destinationKindOptions: reject(
|
||||
[...destinationKindOptions, options.byId[origin.id]],
|
||||
o => o.id === newOption
|
||||
)
|
||||
});
|
||||
|
||||
initSwap({ origin: newOrigin, destination: newDest });
|
||||
};
|
||||
|
||||
public onChangeDestinationKind = (newOption: WhitelistedCoins) => {
|
||||
const { initSwap } = this.props;
|
||||
const { origin, destination } = this.state;
|
||||
const newOriginAmount = () => {
|
||||
const pairName = combineAndUpper(newOption, origin.id);
|
||||
const bityRate = this.props.bityRates.byId[pairName].rate;
|
||||
return bityRate * destination.amount;
|
||||
|
||||
const newOrigin = {
|
||||
...origin,
|
||||
amount: ''
|
||||
};
|
||||
|
||||
const newDest = { ...destination, id: newOption, amount: '' };
|
||||
this.setState({
|
||||
origin: {
|
||||
...origin,
|
||||
amount: newOriginAmount() ? newOriginAmount() : origin.amount
|
||||
},
|
||||
destination: { ...destination, id: newOption }
|
||||
origin: newOrigin,
|
||||
destination: newDest
|
||||
});
|
||||
|
||||
initSwap({ origin: newOrigin, destination: newDest });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { bityRates } = this.props;
|
||||
const { bityRates, shapeshiftRates, provider } = this.props;
|
||||
const {
|
||||
origin,
|
||||
destination,
|
||||
originKindOptions,
|
||||
destinationKindOptions,
|
||||
originErr,
|
||||
destinationErr
|
||||
destinationErr,
|
||||
timeout
|
||||
} = this.state;
|
||||
|
||||
const OriginKindDropDown = Dropdown as new () => Dropdown<any>;
|
||||
const DestinationKindDropDown = Dropdown as new () => Dropdown<typeof destination.id>;
|
||||
const OriginKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
|
||||
const DestinationKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
|
||||
const pairName = combineAndUpper(origin.id, destination.id);
|
||||
const bityLoaded = bityRates.byId[pairName] ? bityRates.byId[pairName].id : false;
|
||||
const bityLoaded = bityRates.byId && bityRates.byId[pairName] ? true : false;
|
||||
const shapeshiftLoaded = shapeshiftRates.byId && shapeshiftRates.byId[pairName] ? true : false;
|
||||
// This ensures both are loaded
|
||||
const loaded = provider === 'shapeshift' ? shapeshiftLoaded : bityLoaded && shapeshiftLoaded;
|
||||
const timeoutLoaded = (bityLoaded && timeout) || (shapeshiftLoaded && timeout);
|
||||
return (
|
||||
<article className="CurrencySwap">
|
||||
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
|
||||
{bityLoaded ? (
|
||||
{loaded || timeoutLoaded ? (
|
||||
<div className="form-inline CurrencySwap-inner-wrap">
|
||||
<div className="CurrencySwap-input-group">
|
||||
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
|
||||
<input
|
||||
id="origin-swap-input"
|
||||
className={`CurrencySwap-input form-control ${
|
||||
String(origin.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
|
||||
String(origin.amount) !== '' &&
|
||||
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'
|
||||
}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={isNaN(origin.amount) ? '' : origin.amount}
|
||||
value={isNaN(origin.amount as number) ? '' : origin.amount}
|
||||
onChange={this.onChangeAmount}
|
||||
/>
|
||||
<div className="CurrencySwap-dropdown">
|
||||
|
@ -239,7 +351,6 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
options={originKindOptions}
|
||||
value={origin.id}
|
||||
onChange={this.onChangeOriginKind}
|
||||
color="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -251,13 +362,14 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
<input
|
||||
id="destination-swap-input"
|
||||
className={`CurrencySwap-input form-control ${
|
||||
String(destination.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
|
||||
String(destination.amount) !== '' &&
|
||||
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'
|
||||
}`}
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={isNaN(destination.amount) ? '' : destination.amount}
|
||||
value={isNaN(destination.amount as number) ? '' : destination.amount}
|
||||
onChange={this.onChangeAmount}
|
||||
/>
|
||||
<div className="CurrencySwap-dropdown">
|
||||
|
@ -266,7 +378,6 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
options={destinationKindOptions}
|
||||
value={destination.id}
|
||||
onChange={this.onChangeDestinationKind}
|
||||
color="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
&-panel {
|
||||
position: relative;
|
||||
margin: 0 auto $space * 2;
|
||||
margin: 0 auto 0;
|
||||
background: linear-gradient(150deg, $ether-blue, $ether-navy);
|
||||
@include mono;
|
||||
|
||||
|
|
|
@ -1,84 +1,136 @@
|
|||
import { NormalizedBityRate } from 'reducers/swap/types';
|
||||
import {
|
||||
NormalizedBityRates,
|
||||
NormalizedShapeshiftRates,
|
||||
NormalizedShapeshiftRate
|
||||
} from 'reducers/swap/types';
|
||||
import bityLogoWhite from 'assets/images/logo-bity-white.svg';
|
||||
import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import { bityReferralURL } from 'config/data';
|
||||
import { bityReferralURL, shapeshiftReferralURL } from 'config/data';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
import './CurrentRates.scss';
|
||||
import { SHAPESHIFT_WHITELIST } from 'api/shapeshift';
|
||||
import { ProviderName } from 'actions/swap';
|
||||
import sample from 'lodash/sample';
|
||||
import times from 'lodash/times';
|
||||
import Rates from './Rates';
|
||||
|
||||
interface Props {
|
||||
[id: string]: NormalizedBityRate;
|
||||
provider: ProviderName;
|
||||
bityRates: NormalizedBityRates;
|
||||
shapeshiftRates: NormalizedShapeshiftRates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
ETHBTCAmount: number;
|
||||
ETHREPAmount: number;
|
||||
BTCETHAmount: number;
|
||||
BTCREPAmount: number;
|
||||
}
|
||||
export default class CurrentRates extends Component<Props> {
|
||||
private shapeShiftRateCache = null;
|
||||
|
||||
export default class CurrentRates extends Component<Props, State> {
|
||||
public state = {
|
||||
ETHBTCAmount: 1,
|
||||
ETHREPAmount: 1,
|
||||
BTCETHAmount: 1,
|
||||
BTCREPAmount: 1
|
||||
public getRandomSSPairData = (
|
||||
shapeshiftRates: NormalizedShapeshiftRates
|
||||
): NormalizedShapeshiftRate => {
|
||||
const coinOne = sample(SHAPESHIFT_WHITELIST) as string;
|
||||
const coinTwo = sample(SHAPESHIFT_WHITELIST) as string;
|
||||
const pair = coinOne + coinTwo;
|
||||
const pairData = shapeshiftRates.byId[pair];
|
||||
if (pairData) {
|
||||
return pairData;
|
||||
} else {
|
||||
// if random pairing is unavailable / missing in state
|
||||
return this.getRandomSSPairData(shapeshiftRates);
|
||||
}
|
||||
};
|
||||
|
||||
public onChange = (event: any) => {
|
||||
const { value } = event.target;
|
||||
const { name } = event.target;
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
public buildSSPairs = (shapeshiftRates: NormalizedShapeshiftRates, n: number = 4) => {
|
||||
const pairCollection = times(n, () => this.getRandomSSPairData(shapeshiftRates));
|
||||
const byId = pairCollection.reduce((acc, cur) => {
|
||||
acc[cur.id] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
const allIds = pairCollection.map(SSData => SSData.id);
|
||||
return {
|
||||
byId,
|
||||
allIds
|
||||
};
|
||||
};
|
||||
|
||||
public buildPairRate = (origin: string, destination: string) => {
|
||||
const pair = origin + destination;
|
||||
const statePair = this.state[(pair + 'Amount') as keyof State];
|
||||
const propsPair = this.props[pair] ? this.props[pair].rate : null;
|
||||
return (
|
||||
<div className="SwapRates-panel-rate">
|
||||
{propsPair ? (
|
||||
<div>
|
||||
<input
|
||||
className="SwapRates-panel-rate-input"
|
||||
onChange={this.onChange}
|
||||
value={statePair}
|
||||
name={pair + 'Amount'}
|
||||
/>
|
||||
<span className="SwapRates-panel-rate-amount">
|
||||
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${destination}`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Spinner size="x1" light={true} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
public isValidRates = rates => {
|
||||
return rates && rates.allIds && rates.allIds.length > 0;
|
||||
};
|
||||
|
||||
public render() {
|
||||
public setupRates = () => {
|
||||
const { shapeshiftRates, bityRates, provider } = this.props;
|
||||
|
||||
let fixedRates;
|
||||
if (provider === 'bity') {
|
||||
fixedRates = bityRates;
|
||||
} else if (provider === 'shapeshift') {
|
||||
// if ShapeShift rates are valid, filter to 4 random pairs
|
||||
if (this.isValidRates(shapeshiftRates)) {
|
||||
if (!this.shapeShiftRateCache) {
|
||||
fixedRates = this.buildSSPairs(shapeshiftRates);
|
||||
this.shapeShiftRateCache = fixedRates;
|
||||
} else {
|
||||
fixedRates = this.shapeShiftRateCache;
|
||||
}
|
||||
} else {
|
||||
// else, pass along invalid rates. Child component will handle showing spinner until they become valid
|
||||
fixedRates = shapeshiftRates;
|
||||
}
|
||||
}
|
||||
|
||||
return fixedRates;
|
||||
};
|
||||
|
||||
public swapEl = (providerURL, providerLogo, children) => {
|
||||
return (
|
||||
<article className="SwapRates">
|
||||
<h3 className="SwapRates-title">{translate('SWAP_rates')}</h3>
|
||||
|
||||
<section className="SwapRates-panel row">
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
{this.buildPairRate('ETH', 'BTC')}
|
||||
{this.buildPairRate('ETH', 'REP')}
|
||||
</div>
|
||||
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
{this.buildPairRate('BTC', 'ETH')}
|
||||
{this.buildPairRate('BTC', 'REP')}
|
||||
</div>
|
||||
<a className="SwapRates-panel-logo" href={bityReferralURL} target="_blank">
|
||||
<img src={bityLogoWhite} width={120} height={49} />
|
||||
{children}
|
||||
<a className="SwapRates-panel-logo" href={providerURL} target="_blank">
|
||||
<img src={providerLogo} width={120} height={49} />
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { provider } = this.props;
|
||||
const rates = this.setupRates();
|
||||
const providerLogo = provider === 'shapeshift' ? shapeshiftLogoWhite : bityLogoWhite;
|
||||
const providerURL = provider === 'shapeshift' ? shapeshiftReferralURL : bityReferralURL;
|
||||
|
||||
let children;
|
||||
|
||||
if (this.isValidRates(rates)) {
|
||||
children = <Rates provider={provider} rates={rates} />;
|
||||
} else {
|
||||
// TODO - de-dup
|
||||
children = (
|
||||
<>
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
<div className="SwapRates-panel-rate">
|
||||
<Spinner size="x1" light={true} />
|
||||
</div>
|
||||
<div className="SwapRates-panel-rate">
|
||||
<Spinner size="x1" light={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
<div className="SwapRates-panel-rate">
|
||||
<Spinner size="x1" light={true} />
|
||||
</div>
|
||||
<div className="SwapRates-panel-rate">
|
||||
<Spinner size="x1" light={true} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return this.swapEl(providerURL, providerLogo, children);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import React, { Component } from 'react';
|
||||
import { AmountFieldFactory } from 'components/AmountFieldFactory';
|
||||
import { GasFieldFactory } from 'components/GasFieldFactory';
|
||||
import { AddressFieldFactory } from 'components/AddressFieldFactory';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
import { Aux } from 'components/ui';
|
||||
import { GenerateTransaction, SendButton, SigningStatus } from 'components';
|
||||
import { resetWallet, TResetWallet } from 'actions/wallet';
|
||||
import translate from 'translations';
|
||||
import { getUnit } from 'selectors/transaction';
|
||||
|
||||
interface StateProps {
|
||||
unit: string;
|
||||
resetWallet: TResetWallet;
|
||||
}
|
||||
|
||||
type Props = StateProps;
|
||||
class FieldsClass extends Component<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="Tab-content-pane">
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<button
|
||||
className="Deploy-field-reset btn btn-default btn-sm"
|
||||
onClick={this.changeWallet}
|
||||
>
|
||||
<i className="fa fa-refresh" />
|
||||
{translate('Change Wallet')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-xs-12">
|
||||
<AddressFieldFactory
|
||||
withProps={({ currentTo }) => (
|
||||
<input className="form-control" type="text" value={currentTo.raw} readOnly={true} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-xs-1" />
|
||||
</div>
|
||||
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<label>{translate('SEND_amount')}</label>
|
||||
|
||||
<AmountFieldFactory
|
||||
withProps={({ currentValue, isValid }) => (
|
||||
<Aux>
|
||||
{!isValid && (
|
||||
<h5 style={{ color: 'red' }}>
|
||||
WARNING: Your ether or token balance is not high enough to complete this
|
||||
transaction! Please send more funds or switch to a different wallet
|
||||
</h5>
|
||||
)}
|
||||
{isValid && (
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
value={`${currentValue.raw} ${this.props.unit}`}
|
||||
readOnly={true}
|
||||
/>
|
||||
)}
|
||||
</Aux>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<label>{translate('TRANS_gas')} </label>
|
||||
|
||||
<GasFieldFactory
|
||||
withProps={({ gasLimit }) => (
|
||||
<input className="form-control" type="text" value={gasLimit.raw} readOnly={true} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SigningStatus />
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12 clearfix">
|
||||
<GenerateTransaction />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row form-group">
|
||||
<SendButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private changeWallet = () => {
|
||||
this.props.resetWallet();
|
||||
};
|
||||
}
|
||||
|
||||
export const Fields = connect((state: AppState) => ({ unit: getUnit(state) }), { resetWallet })(
|
||||
FieldsClass
|
||||
);
|
|
@ -0,0 +1,62 @@
|
|||
import React, { Component } from 'react';
|
||||
import WalletDecrypt from 'components/WalletDecrypt';
|
||||
import { OnlyUnlocked } from 'components/renderCbs';
|
||||
import { Aux } from 'components/ui';
|
||||
import { Fields } from './Fields';
|
||||
import { isUnlocked as isUnlockedSelector } from 'selectors/wallet';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { configureLiteSend, TConfigureLiteSend } from 'actions/swap';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { shouldDisplayLiteSend } from 'selectors/swap';
|
||||
import { NetworkConfig } from 'config/data';
|
||||
|
||||
interface DispatchProps {
|
||||
configureLiteSend: TConfigureLiteSend;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
shouldDisplay: boolean;
|
||||
isUnlocked: boolean;
|
||||
network: NetworkConfig;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps;
|
||||
class LiteSendClass extends Component<Props> {
|
||||
public componentDidMount() {
|
||||
this.props.configureLiteSend();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.props.shouldDisplay) {
|
||||
return null;
|
||||
}
|
||||
const { network, isUnlocked } = this.props;
|
||||
let renderMe;
|
||||
if (network.chainId !== 1) {
|
||||
renderMe = (
|
||||
<div className="row">
|
||||
<div className="col-xs-8 col-xs-push-2 text-center">
|
||||
<h5 style={{ color: 'red' }}>
|
||||
WARNING: You are currently not on the Ethereum Mainnet. Please switch nodes in order
|
||||
for the token swap to function as intended.
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
renderMe = isUnlocked ? <OnlyUnlocked whenUnlocked={<Fields />} /> : <WalletDecrypt />;
|
||||
}
|
||||
|
||||
return <Aux>{renderMe}</Aux>;
|
||||
}
|
||||
}
|
||||
|
||||
export const LiteSend = connect(
|
||||
(state: AppState) => ({
|
||||
shouldDisplay: shouldDisplayLiteSend(state),
|
||||
isUnlocked: isUnlockedSelector(state),
|
||||
network: getNetworkConfig(state)
|
||||
}),
|
||||
{ configureLiteSend }
|
||||
)(LiteSendClass);
|
|
@ -0,0 +1 @@
|
|||
export * from './LiteSend';
|
|
@ -3,14 +3,17 @@ import {
|
|||
TRestartSwap,
|
||||
TStartOrderTimerSwap,
|
||||
TStartPollBityOrderStatus,
|
||||
TStartPollShapeshiftOrderStatus,
|
||||
TStopOrderTimerSwap,
|
||||
TStopPollBityOrderStatus
|
||||
TStopPollBityOrderStatus,
|
||||
TStopPollShapeshiftOrderStatus
|
||||
} from 'actions/swap';
|
||||
import { SwapInput } from 'reducers/swap/types';
|
||||
import React, { Component } from 'react';
|
||||
import BitcoinQR from './BitcoinQR';
|
||||
import PaymentInfo from './PaymentInfo';
|
||||
import SwapProgress from './SwapProgress';
|
||||
import { LiteSend } from './LiteSend';
|
||||
|
||||
interface ReduxStateProps {
|
||||
destinationAddress: string;
|
||||
|
@ -19,7 +22,9 @@ interface ReduxStateProps {
|
|||
reference: string;
|
||||
secondsRemaining: number | null;
|
||||
paymentAddress: string | null;
|
||||
orderStatus: string | null;
|
||||
provider: string;
|
||||
bityOrderStatus: string | null;
|
||||
shapeshiftOrderStatus: string | null;
|
||||
outputTx: any;
|
||||
}
|
||||
|
||||
|
@ -27,20 +32,28 @@ interface ReduxActionProps {
|
|||
restartSwap: TRestartSwap;
|
||||
startOrderTimerSwap: TStartOrderTimerSwap;
|
||||
startPollBityOrderStatus: TStartPollBityOrderStatus;
|
||||
stopOrderTimerSwap: TStopOrderTimerSwap;
|
||||
stopPollBityOrderStatus: TStopPollBityOrderStatus;
|
||||
startPollShapeshiftOrderStatus: TStartPollShapeshiftOrderStatus;
|
||||
stopPollShapeshiftOrderStatus: TStopPollShapeshiftOrderStatus;
|
||||
stopOrderTimerSwap: TStopOrderTimerSwap;
|
||||
showNotification: TShowNotification;
|
||||
}
|
||||
|
||||
export default class PartThree extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
||||
public componentDidMount() {
|
||||
this.props.startPollBityOrderStatus();
|
||||
const { provider } = this.props;
|
||||
if (provider === 'shapeshift') {
|
||||
this.props.startPollShapeshiftOrderStatus();
|
||||
} else {
|
||||
this.props.startPollBityOrderStatus();
|
||||
}
|
||||
this.props.startOrderTimerSwap();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.stopOrderTimerSwap();
|
||||
this.props.stopPollBityOrderStatus();
|
||||
this.props.stopPollShapeshiftOrderStatus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -49,7 +62,9 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
|
|||
origin,
|
||||
destination,
|
||||
paymentAddress,
|
||||
orderStatus,
|
||||
provider,
|
||||
bityOrderStatus,
|
||||
shapeshiftOrderStatus,
|
||||
destinationAddress,
|
||||
outputTx,
|
||||
// ACTIONS
|
||||
|
@ -59,7 +74,9 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
|
|||
const SwapProgressProps = {
|
||||
originId: origin.id,
|
||||
destinationId: destination.id,
|
||||
orderStatus,
|
||||
provider,
|
||||
bityOrderStatus,
|
||||
shapeshiftOrderStatus,
|
||||
showNotification,
|
||||
destinationAddress,
|
||||
outputTx
|
||||
|
@ -72,14 +89,19 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
|
|||
|
||||
const BitcoinQRProps = {
|
||||
paymentAddress,
|
||||
destinationAmount: destination.amount
|
||||
destinationAmount: destination.amount as number
|
||||
};
|
||||
|
||||
const OpenOrder = bityOrderStatus === 'OPEN' || shapeshiftOrderStatus === 'no_deposits';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SwapProgress {...SwapProgressProps} />
|
||||
|
||||
<PaymentInfo {...PaymentInfoProps} />
|
||||
{orderStatus === 'OPEN' && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
|
||||
|
||||
<LiteSend />
|
||||
{OpenOrder && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
margin: $space auto 0;
|
||||
max-width: 620px;
|
||||
width: 100%;
|
||||
font-size: $font-size-medium-bump;
|
||||
font-size: $font-size-medium;
|
||||
text-align: center;
|
||||
@include mono;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $screen-sm) {
|
||||
h1 {
|
||||
font-size: $font-size-medium;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { NormalizedRates } from 'reducers/swap/types';
|
||||
import React, { Component } from 'react';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
import './CurrentRates.scss';
|
||||
import { ProviderName } from 'actions/swap';
|
||||
import { objectContainsObjectKeys } from 'utils/helpers';
|
||||
|
||||
interface RateInputProps {
|
||||
rate: number;
|
||||
amount: number | string;
|
||||
pair: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
onChange: any;
|
||||
}
|
||||
|
||||
export const RateInput: React.SFC<RateInputProps> = ({
|
||||
rate,
|
||||
amount,
|
||||
pair,
|
||||
origin,
|
||||
destination,
|
||||
onChange
|
||||
}) => {
|
||||
return amount || amount === 0 || amount === '' ? (
|
||||
<div className="SwapRates-panel-rate">
|
||||
<input
|
||||
className="SwapRates-panel-rate-input"
|
||||
onChange={onChange}
|
||||
value={amount}
|
||||
name={pair}
|
||||
/>
|
||||
<span className="SwapRates-panel-rate-amount">
|
||||
{` ${origin} = ${toFixedIfLarger(+amount * rate, 6)} ${destination}`}
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
provider: ProviderName;
|
||||
rates: NormalizedRates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
pairs: { [pair: string]: number };
|
||||
}
|
||||
|
||||
export default class Rates extends Component<Props, State> {
|
||||
public state = {
|
||||
pairs: {}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.setState({ pairs: this.getPairs() });
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
const newPairs = this.getPairs();
|
||||
// prevents endless loop. if state already contains new pairs, don't set state
|
||||
if (!objectContainsObjectKeys(newPairs, this.state.pairs)) {
|
||||
const pairs = {
|
||||
...this.state.pairs,
|
||||
...newPairs
|
||||
};
|
||||
this.setState({
|
||||
pairs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getPairs = () => {
|
||||
const { rates } = this.props;
|
||||
const { allIds } = rates;
|
||||
return allIds.reduce((acc, cur) => {
|
||||
acc[cur] = 1;
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
public onChange = (event: any) => {
|
||||
const { value } = event.target;
|
||||
const { name } = event.target;
|
||||
this.setState({
|
||||
pairs: {
|
||||
...this.state.pairs,
|
||||
[name]: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public buildRateInputs = () => {
|
||||
const { rates } = this.props;
|
||||
const { pairs } = this.state;
|
||||
|
||||
const fullData: RateInputProps[] = [];
|
||||
|
||||
rates.allIds.forEach(each => {
|
||||
fullData.push({
|
||||
rate: rates.byId[each].rate,
|
||||
amount: pairs[each],
|
||||
pair: each,
|
||||
origin: rates.byId[each].options[0],
|
||||
destination: rates.byId[each].options[1],
|
||||
onChange: this.onChange
|
||||
});
|
||||
});
|
||||
|
||||
// TODO - don't hardcode only first 4 elements of array.
|
||||
// not likely to change until significant UI revamp, so not worth spending time on now
|
||||
return (
|
||||
<div>
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
<RateInput {...fullData[0]} />
|
||||
<RateInput {...fullData[1]} />
|
||||
</div>
|
||||
|
||||
<div className="SwapRates-panel-side col-sm-6">
|
||||
<RateInput {...fullData[2]} />
|
||||
<RateInput {...fullData[3]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return this.buildRateInputs();
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import {
|
|||
TBityOrderCreateRequestedSwap,
|
||||
TChangeStepSwap,
|
||||
TDestinationAddressSwap,
|
||||
TShapeshiftOrderCreateRequestedSwap,
|
||||
TStopLoadBityRatesSwap
|
||||
} from 'actions/swap';
|
||||
import { SwapInput } from 'reducers/swap/types';
|
||||
|
@ -19,6 +20,8 @@ export interface StateProps {
|
|||
destinationId: keyof typeof donationAddressMap;
|
||||
isPostingOrder: boolean;
|
||||
destinationAddress: string;
|
||||
destinationKind: number;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface ActionProps {
|
||||
|
@ -26,6 +29,7 @@ export interface ActionProps {
|
|||
changeStepSwap: TChangeStepSwap;
|
||||
stopLoadBityRatesSwap: TStopLoadBityRatesSwap;
|
||||
bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap;
|
||||
shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
|
||||
}
|
||||
|
||||
export default class ReceivingAddress extends Component<StateProps & ActionProps, {}> {
|
||||
|
@ -35,15 +39,24 @@ export default class ReceivingAddress extends Component<StateProps & ActionProps
|
|||
};
|
||||
|
||||
public onClickPartTwoComplete = () => {
|
||||
const { origin, destinationId } = this.props;
|
||||
const { origin, destinationId, destinationAddress, destinationKind, provider } = this.props;
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
this.props.bityOrderCreateRequestedSwap(
|
||||
origin.amount,
|
||||
this.props.destinationAddress,
|
||||
combineAndUpper(origin.id, destinationId)
|
||||
);
|
||||
if (provider === 'shapeshift') {
|
||||
this.props.shapeshiftOrderCreateRequestedSwap(
|
||||
destinationAddress,
|
||||
origin.id,
|
||||
destinationId,
|
||||
destinationKind
|
||||
);
|
||||
} else {
|
||||
this.props.bityOrderCreateRequestedSwap(
|
||||
origin.amount as number,
|
||||
this.props.destinationAddress,
|
||||
combineAndUpper(origin.id, destinationId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
@ -77,7 +90,11 @@ export default class ReceivingAddress extends Component<StateProps & ActionProps
|
|||
type="text"
|
||||
value={destinationAddress}
|
||||
onChange={this.onChangeDestinationAddress}
|
||||
placeholder={donationAddressMap[destinationId]}
|
||||
placeholder={
|
||||
destinationId === 'BTC'
|
||||
? donationAddressMap[destinationId]
|
||||
: donationAddressMap.ETH
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
.ShapeshiftBanner {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0px 16px;
|
||||
background-color: #3a526d;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 3px 8px 0 rgba(0,0,0,0.1), inset 0 0 3px 0 rgba(0,0,0,0.1);
|
||||
p {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
box-sizing: content-box;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import './ShapeshiftBanner.scss';
|
||||
import shapeshiftSvg from 'assets/images/logo-shapeshift.svg';
|
||||
|
||||
export default () => (
|
||||
<div className="ShapeshiftBanner">
|
||||
<p>
|
||||
<b style={{ paddingRight: '8px' }}>New Feature: </b>Exchange coins & tokens
|
||||
</p>
|
||||
<img src={shapeshiftSvg} />
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
.SupportFooter {
|
||||
text-align: center;
|
||||
padding-top: 80px;
|
||||
&-fallback {
|
||||
padding: 20px 0;
|
||||
textarea {
|
||||
max-width: 35rem;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import './SupportFooter.scss';
|
||||
import { SwapInput } from 'actions/swap';
|
||||
import { NormalizedBityRates, NormalizedShapeshiftRates } from 'reducers/swap/types';
|
||||
|
||||
interface Props {
|
||||
origin: SwapInput;
|
||||
destination: SwapInput;
|
||||
destinationAddress: string | null;
|
||||
paymentAddress: string | null;
|
||||
reference: string | null;
|
||||
provider: string;
|
||||
shapeshiftRates: NormalizedShapeshiftRates;
|
||||
bityRates: NormalizedBityRates;
|
||||
}
|
||||
|
||||
class SupportFooter extends React.Component<Props, {}> {
|
||||
public state = {
|
||||
open: false
|
||||
};
|
||||
public render() {
|
||||
const { open } = this.state;
|
||||
const {
|
||||
origin,
|
||||
destination,
|
||||
destinationAddress,
|
||||
paymentAddress,
|
||||
reference,
|
||||
provider,
|
||||
shapeshiftRates,
|
||||
bityRates
|
||||
} = this.props;
|
||||
const pair = origin && destination ? origin.id + destination.id : 'BTCETH';
|
||||
const rates = provider === 'shapeshift' ? shapeshiftRates.byId : bityRates.byId;
|
||||
const emailTo =
|
||||
provider === 'shapeshift'
|
||||
? 'support@myetherwallet.com'
|
||||
: 'support@myetherwallet.com,mew@bity.com';
|
||||
const mailSubject = encodeURI('Issue regarding my Swap via MEW');
|
||||
const serviceProvider = provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||
let mailBody;
|
||||
let fallbackBody;
|
||||
if (pair && rates && rates[pair]) {
|
||||
mailBody = encodeURI(`Please include the below if this issue is regarding your order.
|
||||
|
||||
Provider: ${serviceProvider}
|
||||
|
||||
REF ID#: ${reference || ''}
|
||||
|
||||
Amount to send: ${origin.amount || ''} ${origin.id}
|
||||
|
||||
Amount to receive: ${destination.amount || ''} ${destination.id}
|
||||
|
||||
Payment Address: ${paymentAddress || ''}
|
||||
|
||||
Receiving Address: ${destinationAddress || ''}
|
||||
|
||||
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}
|
||||
`);
|
||||
fallbackBody = `To: ${emailTo}
|
||||
Subject: Issue regarding my Swap via MEW
|
||||
Message:
|
||||
Provider: ${serviceProvider}
|
||||
REF ID#: ${reference || ''}
|
||||
Amount to send: ${origin.amount || ''} ${origin.id}
|
||||
Amount to receive: ${destination.amount || ''} ${destination.id}
|
||||
Payment Address: ${paymentAddress || ''}
|
||||
Receiving Address: ${destinationAddress || ''}
|
||||
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}`;
|
||||
}
|
||||
return (
|
||||
<section className="SupportFooter">
|
||||
<a
|
||||
className="btn-warning btn-sm"
|
||||
href={`mailto:${emailTo}?Subject=${mailSubject}&Body=${mailBody}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Issue with your Swap? Contact support
|
||||
</a>
|
||||
<div className="SupportFooter-fallback">
|
||||
<p onClick={this.toggleFallback}>
|
||||
<small>Click here if link doesn't work</small>
|
||||
</p>
|
||||
{open ? (
|
||||
<textarea defaultValue={fallbackBody} className="form-control input-sm" rows={9} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
private toggleFallback = () => {
|
||||
this.setState({
|
||||
open: !this.state.open
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SupportFooter;
|
|
@ -75,6 +75,12 @@ $top-height: 40px;
|
|||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
&-shapeshift {
|
||||
overflow: auto;
|
||||
white-space: normal;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { RestartSwapAction } from 'actions/swap';
|
|||
import { SwapInput } from 'reducers/swap/types';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import classnames from 'classnames';
|
||||
import { toFixedIfLarger } from 'utils/formatters';
|
||||
import './SwapInfoHeader.scss';
|
||||
import SwapInfoHeaderTitle from './SwapInfoHeaderTitle';
|
||||
|
@ -11,6 +12,7 @@ export interface SwapInfoHeaderProps {
|
|||
destination: SwapInput;
|
||||
reference: string;
|
||||
secondsRemaining: number | null;
|
||||
provider: string;
|
||||
restartSwap(): RestartSwapAction;
|
||||
}
|
||||
|
||||
|
@ -20,7 +22,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
|||
if (!origin.amount || !destination.amount) {
|
||||
return;
|
||||
}
|
||||
return destination.amount / origin.amount;
|
||||
return (destination.amount as number) / (origin.amount as number);
|
||||
};
|
||||
|
||||
public isExpanded = () => {
|
||||
|
@ -51,10 +53,18 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
|||
|
||||
public render() {
|
||||
const computedOriginDestinationRatio = this.computedOriginDestinationRatio();
|
||||
const { reference, origin, destination, restartSwap } = this.props;
|
||||
const { reference, origin, destination, restartSwap, provider } = this.props;
|
||||
const SwapInfoHeaderTitleProps = {
|
||||
restartSwap,
|
||||
provider
|
||||
};
|
||||
const referenceClass = classnames(
|
||||
provider === 'shapeshift' && 'SwapInfo-details-block-shapeshift',
|
||||
'SwapInfo-details-block-value'
|
||||
);
|
||||
return (
|
||||
<div className="SwapInfo">
|
||||
<SwapInfoHeaderTitle restartSwap={restartSwap} />
|
||||
<SwapInfoHeaderTitle {...SwapInfoHeaderTitleProps} />
|
||||
<section className="SwapInfo-details row">
|
||||
{/*Amount to send*/}
|
||||
{!this.isExpanded() && (
|
||||
|
@ -67,7 +77,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
|||
{/*Reference Number*/}
|
||||
{this.isExpanded() && (
|
||||
<div className={this.computedClass()}>
|
||||
<h3 className="SwapInfo-details-block-value">{reference}</h3>
|
||||
<h3 className={referenceClass}>{reference}</h3>
|
||||
<p className="SwapInfo-details-block-label">{translate('SWAP_ref_num')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
@ -83,7 +93,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
|||
{/*Amount to Receive*/}
|
||||
<div className={this.computedClass()}>
|
||||
<h3 className="SwapInfo-details-block-value">
|
||||
{` ${destination.amount} ${destination.id}`}
|
||||
{` ${toFixedIfLarger(destination.amount as number, 4)} ${destination.id}`}
|
||||
</h3>
|
||||
<p className="SwapInfo-details-block-label">{translate('SWAP_rec_amt')}</p>
|
||||
</div>
|
||||
|
@ -92,7 +102,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
|
|||
<div className={this.computedClass()}>
|
||||
<h3 className="SwapInfo-details-block-value">
|
||||
{`${computedOriginDestinationRatio &&
|
||||
toFixedIfLarger(computedOriginDestinationRatio)} ${destination.id}/${origin.id}`}
|
||||
toFixedIfLarger(computedOriginDestinationRatio, 4)} ${destination.id}/${origin.id}`}
|
||||
</h3>
|
||||
<p className="SwapInfo-details-block-label">{translate('SWAP_your_rate')}</p>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import { RestartSwapAction } from 'actions/swap';
|
||||
import bityLogo from 'assets/images/logo-bity.svg';
|
||||
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
|
||||
import { bityReferralURL } from 'config/data';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import './SwapInfoHeader.scss';
|
||||
|
||||
export interface SwapInfoHeaderTitleProps {
|
||||
provider: string;
|
||||
restartSwap(): RestartSwapAction;
|
||||
}
|
||||
|
||||
export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitleProps, {}> {
|
||||
public render() {
|
||||
const { provider } = this.props;
|
||||
const logoToRender = provider === 'shapeshift' ? shapeshiftLogo : bityLogo;
|
||||
return (
|
||||
<section className="SwapInfo-top row text-center">
|
||||
<div className="col-xs-3 text-left">
|
||||
|
@ -24,7 +28,7 @@ export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitlePr
|
|||
</div>
|
||||
<div className="col-xs-3">
|
||||
<a className="SwapInfo-top-logo" href={bityReferralURL} target="_blank" rel="noopener">
|
||||
<img className="SwapInfo-top-logo-img" src={bityLogo} />
|
||||
<img className="SwapInfo-top-logo-img" src={logoToRender} />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -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<Props, State> {
|
|||
|
||||
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<HTMLAnchorElement>;
|
||||
let link;
|
||||
|
@ -40,7 +50,7 @@ export default class SwapProgress extends Component<Props, State> {
|
|||
link = bityConfig.ETHTxExplorer(outputTx);
|
||||
linkElement = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
${notificationMessage}
|
||||
{notificationMessage}
|
||||
</a>
|
||||
);
|
||||
// BTC uses a different explorer
|
||||
|
@ -48,7 +58,7 @@ export default class SwapProgress extends Component<Props, State> {
|
|||
link = bityConfig.BTCTxExplorer(outputTx);
|
||||
linkElement = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
${notificationMessage}
|
||||
{notificationMessage}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@ -60,11 +70,12 @@ export default class SwapProgress extends Component<Props, State> {
|
|||
};
|
||||
|
||||
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<Props, State> {
|
|||
} else {
|
||||
return cssClass;
|
||||
}
|
||||
case 'received':
|
||||
case 'RCVE':
|
||||
if (step < 4) {
|
||||
return cssClass + ' is-complete';
|
||||
|
@ -81,9 +93,11 @@ export default class SwapProgress extends Component<Props, State> {
|
|||
} else {
|
||||
return cssClass;
|
||||
}
|
||||
case 'complete':
|
||||
case 'FILL':
|
||||
cssClass += ' is-complete';
|
||||
return cssClass;
|
||||
case 'failed':
|
||||
case 'CANC':
|
||||
return cssClass;
|
||||
default:
|
||||
|
|
|
@ -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<ReduxActionProps & ReduxStateProps, {}> {
|
||||
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<ReduxActionProps & ReduxStateProps, {}> {
|
|||
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<ReduxActionProps & ReduxStateProps, {}> {
|
|||
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<ReduxActionProps & ReduxStateProps, {}> {
|
|||
...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 (
|
||||
<TabSection>
|
||||
<section className="Tab-content swap-tab">
|
||||
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
|
||||
{step === 1 && <ShapeshiftBanner />}
|
||||
{(step === 2 || step === 3) && <SwapInfoHeader {...SwapInfoHeaderProps} />}
|
||||
|
||||
<main className="Tab-content-pane">
|
||||
{step === 1 && <CurrencySwap {...CurrencySwapProps} />}
|
||||
{step === 2 && <ReceivingAddress {...ReceivingAddressProps} />}
|
||||
{step === 3 && <PartThree {...PartThreeProps} />}
|
||||
</main>
|
||||
</section>
|
||||
<SupportFooter {...SupportProps} />
|
||||
</TabSection>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
});
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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];
|
||||
|
|
|
@ -130,8 +130,18 @@ const configureStore = () => {
|
|||
},
|
||||
swap: {
|
||||
...state.swap,
|
||||
options: {},
|
||||
bityRates: {}
|
||||
options: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
},
|
||||
bityRates: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
},
|
||||
shapeshiftRates: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
}
|
||||
},
|
||||
customTokens: state.customTokens
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`render snapshot 1`] = `
|
|||
<Swap
|
||||
bityOrder={Object {}}
|
||||
bityOrderCreateRequestedSwap={[Function]}
|
||||
bityOrderStatus={null}
|
||||
bityRates={
|
||||
Object {
|
||||
"allIds": Array [],
|
||||
|
@ -23,13 +24,13 @@ exports[`render snapshot 1`] = `
|
|||
isFetchingRates={null}
|
||||
isPostingOrder={false}
|
||||
loadBityRatesRequestedSwap={[Function]}
|
||||
loadShapeshiftRatesRequestedSwap={[Function]}
|
||||
options={
|
||||
Object {
|
||||
"allIds": Array [],
|
||||
"byId": Object {},
|
||||
}
|
||||
}
|
||||
orderStatus={null}
|
||||
origin={
|
||||
Object {
|
||||
"amount": NaN,
|
||||
|
@ -38,14 +39,28 @@ exports[`render snapshot 1`] = `
|
|||
}
|
||||
outputTx={null}
|
||||
paymentAddress={null}
|
||||
provider="bity"
|
||||
restartSwap={[Function]}
|
||||
secondsRemaining={null}
|
||||
shapeshiftOrder={Object {}}
|
||||
shapeshiftOrderCreateRequestedSwap={[Function]}
|
||||
shapeshiftOrderStatus={null}
|
||||
shapeshiftRates={
|
||||
Object {
|
||||
"allIds": Array [],
|
||||
"byId": Object {},
|
||||
}
|
||||
}
|
||||
showNotification={[Function]}
|
||||
startOrderTimerSwap={[Function]}
|
||||
startPollBityOrderStatus={[Function]}
|
||||
startPollShapeshiftOrderStatus={[Function]}
|
||||
step={1}
|
||||
stopLoadBityRatesSwap={[Function]}
|
||||
stopLoadShapeshiftRatesSwap={[Function]}
|
||||
stopOrderTimerSwap={[Function]}
|
||||
stopPollBityOrderStatus={[Function]}
|
||||
stopPollShapeshiftOrderStatus={[Function]}
|
||||
swapProvider={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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()));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue