Normalize Swap Reducer (#443)

This commit is contained in:
James Prado 2017-12-11 12:44:53 -05:00 committed by Daniel Ternyak
parent d3210ebc8a
commit 72e30643a9
26 changed files with 502 additions and 812 deletions

View File

@ -2,58 +2,24 @@ import * as interfaces from './actionTypes';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
export type TChangeStepSwap = typeof changeStepSwap; export type TChangeStepSwap = typeof changeStepSwap;
export function changeStepSwap( export function changeStepSwap(payload: number): interfaces.ChangeStepSwapAction {
payload: number
): interfaces.ChangeStepSwapAction {
return { return {
type: TypeKeys.SWAP_STEP, type: TypeKeys.SWAP_STEP,
payload payload
}; };
} }
export type TOriginKindSwap = typeof originKindSwap; export type TInitSwap = typeof initSwap;
export function originKindSwap( export function initSwap(payload: interfaces.SwapInputs): interfaces.InitSwap {
payload: string
): interfaces.OriginKindSwapAction {
return { return {
type: TypeKeys.SWAP_ORIGIN_KIND, type: TypeKeys.SWAP_INIT,
payload
};
}
export type TDestinationKindSwap = typeof destinationKindSwap;
export function destinationKindSwap(
payload: string
): interfaces.DestinationKindSwapAction {
return {
type: TypeKeys.SWAP_DESTINATION_KIND,
payload
};
}
export type TOriginAmountSwap = typeof originAmountSwap;
export function originAmountSwap(
payload?: number | null
): interfaces.OriginAmountSwapAction {
return {
type: TypeKeys.SWAP_ORIGIN_AMOUNT,
payload
};
}
export type TDestinationAmountSwap = typeof destinationAmountSwap;
export function destinationAmountSwap(
payload?: number | null
): interfaces.DestinationAmountSwapAction {
return {
type: TypeKeys.SWAP_DESTINATION_AMOUNT,
payload payload
}; };
} }
export type TLoadBityRatesSucceededSwap = typeof loadBityRatesSucceededSwap; export type TLoadBityRatesSucceededSwap = typeof loadBityRatesSucceededSwap;
export function loadBityRatesSucceededSwap( export function loadBityRatesSucceededSwap(
payload: interfaces.Pairs payload: interfaces.ApiResponse
): interfaces.LoadBityRatesSucceededSwapAction { ): interfaces.LoadBityRatesSucceededSwapAction {
return { return {
type: TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED, type: TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED,
@ -62,9 +28,7 @@ export function loadBityRatesSucceededSwap(
} }
export type TDestinationAddressSwap = typeof destinationAddressSwap; export type TDestinationAddressSwap = typeof destinationAddressSwap;
export function destinationAddressSwap( export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction {
payload?: string
): interfaces.DestinationAddressSwapAction {
return { return {
type: TypeKeys.SWAP_DESTINATION_ADDRESS, type: TypeKeys.SWAP_DESTINATION_ADDRESS,
payload payload
@ -93,9 +57,7 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction
} }
export type TOrderTimeSwap = typeof orderTimeSwap; export type TOrderTimeSwap = typeof orderTimeSwap;
export function orderTimeSwap( export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction {
payload: number
): interfaces.OrderSwapTimeSwapAction {
return { return {
type: TypeKeys.SWAP_ORDER_TIME, type: TypeKeys.SWAP_ORDER_TIME,
payload payload

View File

@ -1,4 +1,5 @@
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
export interface Pairs { export interface Pairs {
ETHBTC: number; ETHBTC: number;
ETHREP: number; ETHREP: number;
@ -6,29 +7,38 @@ export interface Pairs {
BTCREP: number; BTCREP: number;
} }
export interface OriginKindSwapAction { export interface SwapInput {
type: TypeKeys.SWAP_ORIGIN_KIND; id: string;
payload: string; amount: number;
} }
export interface DestinationKindSwapAction { export interface SwapInputs {
type: TypeKeys.SWAP_DESTINATION_KIND; origin: SwapInput;
payload: string; destination: SwapInput;
} }
export interface OriginAmountSwapAction { export interface InitSwap {
type: TypeKeys.SWAP_ORIGIN_AMOUNT; type: TypeKeys.SWAP_INIT;
payload?: number | null; payload: SwapInputs;
} }
export interface DestinationAmountSwapAction { export interface Option {
type: TypeKeys.SWAP_DESTINATION_AMOUNT; id: string;
payload?: number | null; }
export interface ApiResponseObj {
id: string;
options: Option[];
rate: number;
}
export interface ApiResponse {
[name: string]: ApiResponseObj;
} }
export interface LoadBityRatesSucceededSwapAction { export interface LoadBityRatesSucceededSwapAction {
type: TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED; type: TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED;
payload: Pairs; payload: ApiResponse;
} }
export interface DestinationAddressSwapAction { export interface DestinationAddressSwapAction {
@ -135,10 +145,7 @@ export interface StopPollBityOrderStatusAction {
/*** Action Type Union ***/ /*** Action Type Union ***/
export type SwapAction = export type SwapAction =
| ChangeStepSwapAction | ChangeStepSwapAction
| OriginKindSwapAction | InitSwap
| DestinationKindSwapAction
| OriginAmountSwapAction
| DestinationAmountSwapAction
| LoadBityRatesSucceededSwapAction | LoadBityRatesSucceededSwapAction
| DestinationAddressSwapAction | DestinationAddressSwapAction
| RestartSwapAction | RestartSwapAction

View File

@ -1,9 +1,6 @@
export enum TypeKeys { export enum TypeKeys {
SWAP_STEP = 'SWAP_STEP', SWAP_STEP = 'SWAP_STEP',
SWAP_ORIGIN_KIND = 'SWAP_ORIGIN_KIND', SWAP_INIT = 'SWAP_INIT',
SWAP_DESTINATION_KIND = 'SWAP_DESTINATION_KIND',
SWAP_ORIGIN_AMOUNT = 'SWAP_ORIGIN_AMOUNT',
SWAP_DESTINATION_AMOUNT = 'SWAP_DESTINATION_AMOUNT',
SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED', SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS', SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS',
SWAP_RESTART = 'SWAP_RESTART', SWAP_RESTART = 'SWAP_RESTART',

View File

@ -1,23 +1,31 @@
import bityConfig from 'config/bity'; import bityConfig, { WhitelistedCoins } from 'config/bity';
import { checkHttpStatus, parseJSON } from './utils'; import { checkHttpStatus, parseJSON, filter } from './utils';
const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => {
return filter(from, arr) && filter(to, arr);
};
export function getAllRates() { export function getAllRates() {
const mappedRates = {}; const mappedRates = {};
return _getAllRates().then(bityRates => { return _getAllRates().then(bityRates => {
bityRates.objects.forEach(each => { bityRates.objects.forEach(each => {
const pairName = each.pair; const pairName = each.pair;
mappedRates[pairName] = parseFloat(each.rate_we_sell); const from = { id: pairName.substring(0, 3) };
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'])) {
mappedRates[pairName] = {
id: pairName,
options: [from, to],
rate: parseFloat(each.rate_we_sell)
};
}
}); });
return mappedRates; return mappedRates;
}); });
} }
export function postOrder( export function postOrder(amount: number, destAddress: string, mode: number, pair: string) {
amount: number,
destAddress: string,
mode: number,
pair: string
) {
return fetch(`${bityConfig.serverURL}/order`, { return fetch(`${bityConfig.serverURL}/order`, {
method: 'post', method: 'post',
body: JSON.stringify({ body: JSON.stringify({

View File

@ -1,3 +1,9 @@
import { indexOf } from 'lodash';
export const filter = (i: any, arr: any[]) => {
return -1 !== indexOf(arr, i) ? true : false;
};
export function checkHttpStatus(response) { export function checkHttpStatus(response) {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response; return response;

View File

@ -31,7 +31,6 @@ export default class SimpleButton extends Component<Props, {}> {
public render() { public render() {
const { loading, disabled, loadingText, text, onClick } = this.props; const { loading, disabled, loadingText, text, onClick } = this.props;
return ( return (
<div> <div>
<button <button
@ -39,13 +38,13 @@ export default class SimpleButton extends Component<Props, {}> {
disabled={loading || disabled} disabled={loading || disabled}
className={this.computedClass()} className={this.computedClass()}
> >
{loading {loading ? (
? <div> <div>
<Spinner /> {loadingText || text} <Spinner /> {loadingText || text}
</div> </div>
: <div> ) : (
{text} <div>{text}</div>
</div>} )}
</button> </button>
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import { BTCTxExplorer, ETHTxExplorer } from './data'; import { BTCTxExplorer, ETHTxExplorer } from './data';
type SupportedDestinationKind = 'ETH' | 'BTC' | 'REP'; export type WhitelistedCoins = 'ETH' | 'BTC' | 'REP';
const serverURL = 'https://bity.myetherapi.com'; const serverURL = 'https://bity.myetherapi.com';
const bityURL = 'https://bity.com/api'; const bityURL = 'https://bity.com/api';
@ -16,19 +16,13 @@ const buffers = {
}; };
// rate must be BTC[KIND] // rate must be BTC[KIND]
export function generateKindMin( export function generateKindMin(BTCKINDRate: number, kind: WhitelistedCoins): number {
BTCKINDRate: number,
kind: SupportedDestinationKind
): number {
const kindMinVal = BTCKINDRate * BTCMin; const kindMinVal = BTCKINDRate * BTCMin;
return kindMinVal + kindMinVal * buffers[kind]; return kindMinVal + kindMinVal * buffers[kind];
} }
// rate must be BTC[KIND] // rate must be BTC[KIND]
export function generateKindMax( export function generateKindMax(BTCKINDRate: number, kind: WhitelistedCoins): number {
BTCKINDRate: number,
kind: SupportedDestinationKind
): number {
const kindMax = BTCKINDRate * BTCMax; const kindMax = BTCKINDRate * BTCMax;
return kindMax - kindMax * buffers[kind]; return kindMax - kindMax * buffers[kind];
} }

View File

@ -3,29 +3,25 @@ import React, { Component } from 'react';
interface Props { interface Props {
paymentAddress: string | null; paymentAddress: string | null;
amount: number | null; destinationAmount: number;
} }
export default class BitcoinQR extends Component<Props, {}> { export default class BitcoinQR extends Component<Props, {}> {
public render() { public render() {
const { paymentAddress, amount } = this.props; const { paymentAddress, destinationAmount } = this.props;
return ( return (
<div> <div>
<section className="row block swap-address text-center"> <section className="row block swap-address text-center">
<label> Your Address </label> <label> Your Address </label>
<div className="qr-code"> <div className="qr-code">
<QRCode value={`bitcoin:${paymentAddress}amount=${amount}`} /> <QRCode value={`bitcoin:${paymentAddress}amount=${destinationAmount}`} />
</div> </div>
<br /> <br />
<p className="text-danger"> <p className="text-danger">
Orders that take too long will have to be processed manually &amp; Orders that take too long will have to be processed manually &amp; and may delay the
and may delay the amount of time it takes to receive your coins. amount of time it takes to receive your coins.
<br /> <br />
<a <a href="https://shapeshift.io/#/btcfee" target="_blank" rel="noopener">
href="https://shapeshift.io/#/btcfee"
target="_blank"
rel="noopener"
>
Please use the recommended TX fees seen here. Please use the recommended TX fees seen here.
</a> </a>
</p> </p>

View File

@ -1,267 +1,218 @@
import { TShowNotification } from 'actions/notifications'; import { TChangeStepSwap, TInitSwap } from 'actions/swap';
import { import { NormalizedBityRates, NormalizedOptions, SwapInput } from 'reducers/swap/types';
TChangeStepSwap,
TDestinationAmountSwap,
TDestinationKindSwap,
TOriginAmountSwap,
TOriginKindSwap
} from 'actions/swap';
import SimpleButton from 'components/ui/SimpleButton'; import SimpleButton from 'components/ui/SimpleButton';
import bityConfig, { generateKindMax, generateKindMin } from 'config/bity'; import bityConfig, { generateKindMax, generateKindMin, WhitelistedCoins } from 'config/bity';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate from 'translations'; import translate from 'translations';
import { combineAndUpper, toFixedIfLarger } from 'utils/formatters'; import { combineAndUpper } from 'utils/formatters';
import './CurrencySwap.scss';
import { Dropdown } from 'components/ui'; import { Dropdown } from 'components/ui';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import { without, intersection } from 'lodash';
import './CurrencySwap.scss';
export interface StateProps { export interface StateProps {
bityRates: any; bityRates: NormalizedBityRates;
originAmount: number | null; options: NormalizedOptions;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
} }
export interface ActionProps { export interface ActionProps {
showNotification: TShowNotification;
changeStepSwap: TChangeStepSwap; changeStepSwap: TChangeStepSwap;
originKindSwap: TOriginKindSwap; initSwap: TInitSwap;
destinationKindSwap: TDestinationKindSwap;
originAmountSwap: TOriginAmountSwap;
destinationAmountSwap: TDestinationAmountSwap;
} }
interface State { interface State {
disabled: boolean; disabled: boolean;
showedMinMaxError: boolean; origin: SwapInput;
destination: SwapInput;
originKindOptions: WhitelistedCoins[];
destinationKindOptions: WhitelistedCoins[];
originErr: string; originErr: string;
destinationErr: string; destinationErr: string;
} }
export default class CurrencySwap extends Component< type Props = StateProps & ActionProps;
StateProps & ActionProps,
State export default class CurrencySwap extends Component<Props, State> {
> {
public state = { public state = {
disabled: true, disabled: true,
showedMinMaxError: false, origin: { id: 'BTC', amount: NaN } as SwapInput,
destination: { id: 'ETH', amount: NaN } as SwapInput,
originKindOptions: ['BTC', 'ETH'] as WhitelistedCoins[],
destinationKindOptions: ['ETH'] as WhitelistedCoins[],
originErr: '', originErr: '',
destinationErr: '' destinationErr: ''
}; };
public componentWillReceiveProps(newProps) { public componentDidUpdate(prevProps: Props, prevState: State) {
const { const { origin, destination } = this.state;
originAmount, const { options } = this.props;
originKind, if (origin !== prevState.origin) {
destinationKind, this.setDisabled(origin, destination);
destinationAmount }
} = newProps; if (options.allIds !== prevProps.options.allIds) {
if ( const originKindOptions: WhitelistedCoins[] = intersection<any>(
originKind !== this.props.originKind || options.allIds,
destinationKind !== this.props.destinationKind this.state.originKindOptions
) {
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmount
); );
const destinationKindOptions: WhitelistedCoins[] = without<any>(options.allIds, origin.id);
this.setState({
originKindOptions,
destinationKindOptions
});
} }
} }
public isMinMaxValid = (amount, kind) => { public getMinMax = (kind: WhitelistedCoins) => {
let bityMin; let min;
let bityMax; let max;
if (kind !== 'BTC') { if (kind !== 'BTC') {
const bityPairRate = this.props.bityRates['BTC' + kind]; const bityPairRate = this.props.bityRates.byId['BTC' + kind].rate;
bityMin = generateKindMin(bityPairRate, kind); min = generateKindMin(bityPairRate, kind);
bityMax = generateKindMax(bityPairRate, kind); max = generateKindMax(bityPairRate, kind);
} else { } else {
bityMin = bityConfig.BTCMin; min = bityConfig.BTCMin;
bityMax = bityConfig.BTCMax; max = bityConfig.BTCMax;
} }
const higherThanMin = amount >= bityMin; return { min, max };
const lowerThanMax = amount <= bityMax; };
public isMinMaxValid = (amount: number, kind: WhitelistedCoins) => {
const rate = this.getMinMax(kind);
const higherThanMin = amount >= rate.min;
const lowerThanMax = amount <= rate.max;
return higherThanMin && lowerThanMax; return higherThanMin && lowerThanMax;
}; };
public isDisabled = (originAmount, originKind, destinationAmount) => { public setDisabled(origin: SwapInput, destination: SwapInput) {
const hasOriginAmountAndDestinationAmount = const amountsValid = origin.amount && destination.amount;
originAmount && destinationAmount; const minMaxValid = this.isMinMaxValid(origin.amount, origin.id);
const minMaxIsValid = this.isMinMaxValid(originAmount, originKind);
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
};
public setDisabled( const disabled = !(amountsValid && minMaxValid);
originAmount,
originKind,
destinationKind,
destinationAmount
) {
const disabled = this.isDisabled(
originAmount,
originKind,
destinationAmount
);
if (disabled && originAmount) { const createErrString = (kind: WhitelistedCoins, amount: number) => {
const { bityRates } = this.props; const rate = this.getMinMax(kind);
const ETHMin = generateKindMin(bityRates.BTCETH, 'ETH');
const ETHMax = generateKindMax(bityRates.BTCETH, 'ETH');
const REPMin = generateKindMin(bityRates.BTCREP, 'REP');
const getRates = kind => {
let minAmount;
let maxAmount;
switch (kind) {
case 'BTC':
minAmount = toFixedIfLarger(bityConfig.BTCMin, 3);
maxAmount = toFixedIfLarger(bityConfig.BTCMax, 3);
break;
case 'ETH':
minAmount = toFixedIfLarger(ETHMin, 3);
maxAmount = toFixedIfLarger(ETHMax, 3);
break;
case 'REP':
minAmount = toFixedIfLarger(REPMin, 3);
break;
default:
if (this.state.showedMinMaxError) {
this.setState(
{
showedMinMaxError: true
},
() => {
this.props.showNotification(
'danger',
"Couldn't get match currency kind. Something went terribly wrong",
10000
);
}
);
}
}
return { minAmount, maxAmount };
};
const createErrString = (kind, amount, rate) => {
let errString; let errString;
if (amount > rate.maxAmount) { if (amount > rate.max) {
errString = `Maximum ${kind} is ${rate.maxAmount} ${kind}`; errString = `Maximum ${rate.max} ${kind}`;
} else { } else {
errString = `Minimum ${kind} is ${rate.minAmount} ${kind}`; errString = `Minimum ${rate.min} ${kind}`;
} }
return errString; return errString;
}; };
const originRate = getRates(originKind);
const destinationRate = getRates(destinationKind); const showError = disabled && amountsValid;
const originErr = createErrString(originKind, originAmount, originRate); const originErr = showError ? createErrString(origin.id, origin.amount) : '';
const destinationErr = createErrString( const destinationErr = showError ? createErrString(destination.id, destination.amount) : '';
destinationKind,
destinationAmount,
destinationRate
);
this.setState({ this.setState({
disabled,
originErr, originErr,
destinationErr, destinationErr
disabled: true
}); });
} else {
this.setState({
originErr: '',
destinationErr: '',
disabled
});
}
} }
public onClickStartSwap = () => { public onClickStartSwap = () => {
this.props.changeStepSwap(2); const { origin, destination } = this.state;
const { changeStepSwap, initSwap } = this.props;
initSwap({ origin, destination });
changeStepSwap(2);
}; };
public setOriginAndDestinationToNull = () => { public setOriginAndDestinationToInitialVal = () => {
this.props.originAmountSwap(null); this.setState({
this.props.destinationAmountSwap(null); origin: { ...this.state.origin, amount: NaN },
this.setDisabled( destination: { ...this.state.destination, amount: NaN }
null, });
this.props.originKind,
this.props.destinationKind,
null
);
}; };
public onChangeOriginAmount = ( public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
event: React.SyntheticEvent<HTMLInputElement> if (amount || amount === 0) {
) => { const pairName = combineAndUpper(origin.id, destination.id);
const { destinationKind, originKind } = this.props; const bityRate = this.props.bityRates.byId[pairName].rate;
const amount = (event.target as HTMLInputElement).value; const destinationAmount = amount * bityRate;
const originAmountAsNumber = parseFloat(amount); this.setState({
if (originAmountAsNumber || originAmountAsNumber === 0) { origin: { ...this.state.origin, amount },
const pairName = combineAndUpper(originKind, destinationKind); destination: { ...this.state.destination, amount: destinationAmount }
const bityRate = this.props.bityRates[pairName]; });
this.props.originAmountSwap(originAmountAsNumber);
const destinationAmount = originAmountAsNumber * bityRate;
this.props.destinationAmountSwap(destinationAmount);
this.setDisabled(
originAmountAsNumber,
originKind,
destinationKind,
destinationAmount
);
} else { } else {
this.setOriginAndDestinationToNull(); this.setOriginAndDestinationToInitialVal();
} }
}; };
public onChangeDestinationAmount = ( public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
event: React.SyntheticEvent<HTMLInputElement> if (amount || amount === 0) {
) => { const pairNameReversed = combineAndUpper(destination.id, origin.id);
const { destinationKind, originKind } = this.props; const bityRate = this.props.bityRates.byId[pairNameReversed].rate;
const amount = (event.target as HTMLInputElement).value; const originAmount = amount * bityRate;
const destinationAmountAsNumber = parseFloat(amount); this.setState({
if (destinationAmountAsNumber || destinationAmountAsNumber === 0) { origin: { ...this.state.origin, amount: originAmount },
this.props.destinationAmountSwap(destinationAmountAsNumber); destination: {
const pairNameReversed = combineAndUpper(destinationKind, originKind); ...this.state.destination,
const bityRate = this.props.bityRates[pairNameReversed]; amount
const originAmount = destinationAmountAsNumber * bityRate;
this.props.originAmountSwap(originAmount);
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmountAsNumber
);
} else {
this.setOriginAndDestinationToNull();
} }
});
} else {
this.setOriginAndDestinationToInitialVal();
}
};
public onChangeAmount = (event: React.SyntheticEvent<HTMLInputElement>) => {
const type = (event.target as HTMLInputElement).id;
const { origin, destination } = this.state;
const amount = parseFloat((event.target as HTMLInputElement).value);
type === 'origin-swap-input'
? this.updateOriginAmount(origin, destination, amount)
: this.updateDestinationAmount(origin, destination, amount);
};
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;
};
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)
});
};
public onChangeDestinationKind = (newOption: WhitelistedCoins) => {
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;
};
this.setState({
origin: {
...origin,
amount: newOriginAmount() ? newOriginAmount() : origin.amount
},
destination: { ...destination, id: newOption }
});
}; };
public render() { public render() {
const { bityRates } = this.props;
const { const {
originAmount, origin,
destinationAmount, destination,
originKind,
destinationKind,
destinationKindOptions,
originKindOptions, originKindOptions,
bityRates destinationKindOptions,
} = this.props; originErr,
destinationErr
} = this.state;
const { originErr, destinationErr } = this.state; const OriginKindDropDown = Dropdown as new () => Dropdown<any>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<typeof destination.id>;
const OriginKindDropDown = Dropdown as new () => Dropdown< const pairName = combineAndUpper(origin.id, destination.id);
typeof originKind const bityLoaded = bityRates.byId[pairName] ? bityRates.byId[pairName].id : false;
>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<
typeof destinationKind
>;
const pairName = combineAndUpper(originKind, destinationKind);
const bityLoaded = bityRates[pairName];
return ( return (
<article className="CurrencySwap"> <article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1> <h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
@ -270,25 +221,23 @@ export default class CurrencySwap extends Component<
<div className="CurrencySwap-input-group"> <div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message">{originErr}</span> <span className="CurrencySwap-error-message">{originErr}</span>
<input <input
id="origin-swap-input"
className={`CurrencySwap-input form-control ${ className={`CurrencySwap-input form-control ${
String(originAmount) !== '' && String(origin.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
this.isMinMaxValid(originAmount, originKind)
? 'is-valid' ? 'is-valid'
: 'is-invalid' : 'is-invalid'
}`} }`}
type="number" type="number"
placeholder="Amount" placeholder="Amount"
value={originAmount || originAmount === 0 ? originAmount : ''} value={isNaN(origin.amount) ? '' : origin.amount}
onChange={this.onChangeOriginAmount} onChange={this.onChangeAmount}
/> />
<div className="CurrencySwap-dropdown"> <div className="CurrencySwap-dropdown">
<OriginKindDropDown <OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${ ariaLabel={`change origin kind. current origin kind ${origin.id}`}
originKind
}`}
options={originKindOptions} options={originKindOptions}
value={originKind} value={origin.id}
onChange={this.props.originKindSwap} onChange={this.onChangeOriginKind}
size="smr" size="smr"
color="default" color="default"
/> />
@ -296,33 +245,25 @@ export default class CurrencySwap extends Component<
</div> </div>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1> <h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<div className="CurrencySwap-input-group"> <div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message"> <span className="CurrencySwap-error-message">{destinationErr}</span>
{destinationErr}
</span>
<input <input
id="destination-swap-input"
className={`CurrencySwap-input form-control ${ className={`CurrencySwap-input form-control ${
String(destinationAmount) !== '' && String(destination.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
this.isMinMaxValid(originAmount, originKind)
? 'is-valid' ? 'is-valid'
: 'is-invalid' : 'is-invalid'
}`} }`}
type="number" type="number"
placeholder="Amount" placeholder="Amount"
value={ value={isNaN(destination.amount) ? '' : destination.amount}
destinationAmount || destinationAmount === 0 onChange={this.onChangeAmount}
? destinationAmount
: ''
}
onChange={this.onChangeDestinationAmount}
/> />
<div className="CurrencySwap-dropdown"> <div className="CurrencySwap-dropdown">
<DestinationKindDropDown <DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${ ariaLabel={`change destination kind. current destination kind ${destination.id}`}
destinationKind
}`}
options={destinationKindOptions} options={destinationKindOptions}
value={destinationKind} value={destination.id}
onChange={this.props.destinationKindSwap} onChange={this.onChangeDestinationKind}
size="smr" size="smr"
color="default" color="default"
/> />

View File

@ -1,4 +1,4 @@
import { Pairs } from 'actions/swap'; import { NormalizedBityRate } from 'reducers/swap/types';
import bityLogoWhite from 'assets/images/logo-bity-white.svg'; import bityLogoWhite from 'assets/images/logo-bity-white.svg';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import { bityReferralURL } from 'config/data'; import { bityReferralURL } from 'config/data';
@ -7,13 +7,18 @@ import translate from 'translations';
import { toFixedIfLarger } from 'utils/formatters'; import { toFixedIfLarger } from 'utils/formatters';
import './CurrentRates.scss'; import './CurrentRates.scss';
interface Props {
[id: string]: NormalizedBityRate;
}
interface State { interface State {
ETHBTCAmount: number; ETHBTCAmount: number;
ETHREPAmount: number; ETHREPAmount: number;
BTCETHAmount: number; BTCETHAmount: number;
BTCREPAmount: number; BTCREPAmount: number;
} }
export default class CurrentRates extends Component<Pairs, State> {
export default class CurrentRates extends Component<Props, State> {
public state = { public state = {
ETHBTCAmount: 1, ETHBTCAmount: 1,
ETHREPAmount: 1, ETHREPAmount: 1,
@ -32,7 +37,7 @@ export default class CurrentRates extends Component<Pairs, State> {
public buildPairRate = (origin: string, destination: string) => { public buildPairRate = (origin: string, destination: string) => {
const pair = origin + destination; const pair = origin + destination;
const statePair = this.state[pair + 'Amount']; const statePair = this.state[pair + 'Amount'];
const propsPair = this.props[pair]; const propsPair = this.props[pair] ? this.props[pair].rate : null;
return ( return (
<div className="SwapRates-panel-rate"> <div className="SwapRates-panel-rate">
{propsPair ? ( {propsPair ? (
@ -44,9 +49,7 @@ export default class CurrentRates extends Component<Pairs, State> {
name={pair + 'Amount'} name={pair + 'Amount'}
/> />
<span className="SwapRates-panel-rate-amount"> <span className="SwapRates-panel-rate-amount">
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${ {` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${destination}`}
destination
}`}
</span> </span>
</div> </div>
) : ( ) : (
@ -71,11 +74,7 @@ export default class CurrentRates extends Component<Pairs, State> {
{this.buildPairRate('BTC', 'ETH')} {this.buildPairRate('BTC', 'ETH')}
{this.buildPairRate('BTC', 'REP')} {this.buildPairRate('BTC', 'REP')}
</div> </div>
<a <a className="SwapRates-panel-logo" href={bityReferralURL} target="_blank">
className="SwapRates-panel-logo"
href={bityReferralURL}
target="_blank"
>
<img src={bityLogoWhite} width={120} height={49} /> <img src={bityLogoWhite} width={120} height={49} />
</a> </a>
</section> </section>

View File

@ -6,6 +6,7 @@ import {
TStopOrderTimerSwap, TStopOrderTimerSwap,
TStopPollBityOrderStatus TStopPollBityOrderStatus
} from 'actions/swap'; } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BitcoinQR from './BitcoinQR'; import BitcoinQR from './BitcoinQR';
import PaymentInfo from './PaymentInfo'; import PaymentInfo from './PaymentInfo';
@ -13,10 +14,8 @@ import SwapProgress from './SwapProgress';
interface ReduxStateProps { interface ReduxStateProps {
destinationAddress: string; destinationAddress: string;
destinationKind: string; origin: SwapInput;
originKind: string; destination: SwapInput;
originAmount: number | null;
destinationAmount: number | null;
reference: string; reference: string;
secondsRemaining: number | null; secondsRemaining: number | null;
paymentAddress: string | null; paymentAddress: string | null;
@ -33,10 +32,7 @@ interface ReduxActionProps {
showNotification: TShowNotification; showNotification: TShowNotification;
} }
export default class PartThree extends Component< export default class PartThree extends Component<ReduxActionProps & ReduxStateProps, {}> {
ReduxActionProps & ReduxStateProps,
{}
> {
public componentDidMount() { public componentDidMount() {
this.props.startPollBityOrderStatus(); this.props.startPollBityOrderStatus();
this.props.startOrderTimerSwap(); this.props.startOrderTimerSwap();
@ -50,21 +46,19 @@ export default class PartThree extends Component<
public render() { public render() {
const { const {
// STATE // STATE
originAmount, origin,
originKind, destination,
destinationKind,
paymentAddress, paymentAddress,
orderStatus, orderStatus,
destinationAddress, destinationAddress,
outputTx, outputTx,
destinationAmount,
// ACTIONS // ACTIONS
showNotification showNotification
} = this.props; } = this.props;
const SwapProgressProps = { const SwapProgressProps = {
originKind, originId: origin.id,
destinationKind, destinationId: destination.id,
orderStatus, orderStatus,
showNotification, showNotification,
destinationAddress, destinationAddress,
@ -72,22 +66,20 @@ export default class PartThree extends Component<
}; };
const PaymentInfoProps = { const PaymentInfoProps = {
originKind, origin,
originAmount,
paymentAddress paymentAddress
}; };
const BitcoinQRProps = { const BitcoinQRProps = {
paymentAddress, paymentAddress,
amount: destinationAmount destinationAmount: destination.amount
}; };
return ( return (
<div> <div>
<SwapProgress {...SwapProgressProps} /> <SwapProgress {...SwapProgressProps} />
<PaymentInfo {...PaymentInfoProps} /> <PaymentInfo {...PaymentInfoProps} />
{orderStatus === 'OPEN' && {orderStatus === 'OPEN' && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
originKind === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
</div> </div>
); );
} }

View File

@ -1,22 +1,23 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate from 'translations'; import translate from 'translations';
import { SwapInput } from 'reducers/swap/types';
import './PaymentInfo.scss'; import './PaymentInfo.scss';
export interface Props { export interface Props {
originKind: string; origin: SwapInput;
originAmount: number | null;
paymentAddress: string | null; paymentAddress: string | null;
} }
export default class PaymentInfo extends Component<Props, {}> { export default class PaymentInfo extends Component<Props, {}> {
public render() { public render() {
const { origin } = this.props;
return ( return (
<section className="SwapPayment"> <section className="SwapPayment">
<h1> <h1>
<span>{translate('SWAP_order_CTA')}</span> <span>{translate('SWAP_order_CTA')}</span>
<strong> <strong>
{' '} {' '}
{this.props.originAmount} {this.props.originKind} {origin.amount} {origin.id}
</strong> </strong>
<span> {translate('SENDModal_Content_2')}</span> <span> {translate('SENDModal_Content_2')}</span>
<input <input

View File

@ -4,6 +4,7 @@ import {
TDestinationAddressSwap, TDestinationAddressSwap,
TStopLoadBityRatesSwap TStopLoadBityRatesSwap
} from 'actions/swap'; } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import classnames from 'classnames'; import classnames from 'classnames';
import SimpleButton from 'components/ui/SimpleButton'; import SimpleButton from 'components/ui/SimpleButton';
import { donationAddressMap } from 'config/data'; import { donationAddressMap } from 'config/data';
@ -14,10 +15,9 @@ import { combineAndUpper } from 'utils/formatters';
import './ReceivingAddress.scss'; import './ReceivingAddress.scss';
export interface StateProps { export interface StateProps {
origin: SwapInput;
destinationId: string;
isPostingOrder: boolean; isPostingOrder: boolean;
originAmount: number | null;
originKind: string;
destinationKind: string;
destinationAddress: string; destinationAddress: string;
} }
@ -28,33 +28,29 @@ export interface ActionProps {
bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap; bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap;
} }
export default class ReceivingAddress extends Component< export default class ReceivingAddress extends Component<StateProps & ActionProps, {}> {
StateProps & ActionProps, public onChangeDestinationAddress = (event: React.SyntheticEvent<HTMLInputElement>) => {
{}
> {
public onChangeDestinationAddress = (
event: React.SyntheticEvent<HTMLInputElement>
) => {
const value = (event.target as HTMLInputElement).value; const value = (event.target as HTMLInputElement).value;
this.props.destinationAddressSwap(value); this.props.destinationAddressSwap(value);
}; };
public onClickPartTwoComplete = () => { public onClickPartTwoComplete = () => {
if (!this.props.originAmount) { const { origin, destinationId } = this.props;
if (!origin) {
return; return;
} }
this.props.bityOrderCreateRequestedSwap( this.props.bityOrderCreateRequestedSwap(
this.props.originAmount, origin.amount,
this.props.destinationAddress, this.props.destinationAddress,
combineAndUpper(this.props.originKind, this.props.destinationKind) combineAndUpper(origin.id, destinationId)
); );
}; };
public render() { public render() {
const { destinationKind, destinationAddress, isPostingOrder } = this.props; const { destinationId, destinationAddress, isPostingOrder } = this.props;
let validAddress; let validAddress;
// TODO - find better pattern here once currencies move beyond BTC, ETH, REP // TODO - find better pattern here once currencies move beyond BTC, ETH, REP
if (this.props.destinationKind === 'BTC') { if (destinationId === 'BTC') {
validAddress = isValidBTCAddress(destinationAddress); validAddress = isValidBTCAddress(destinationAddress);
} else { } else {
validAddress = isValidETHAddress(destinationAddress); validAddress = isValidETHAddress(destinationAddress);
@ -73,7 +69,7 @@ export default class ReceivingAddress extends Component<
<div className="col-sm-8 col-sm-offset-2 col-xs-12"> <div className="col-sm-8 col-sm-offset-2 col-xs-12">
<label className="SwapAddress-address"> <label className="SwapAddress-address">
<h4 className="SwapAddress-address-label"> <h4 className="SwapAddress-address-label">
{translate('SWAP_rec_add')} ({destinationKind}) {translate('SWAP_rec_add')} ({destinationId})
</h4> </h4>
<input <input
@ -81,7 +77,7 @@ export default class ReceivingAddress extends Component<
type="text" type="text"
value={destinationAddress} value={destinationAddress}
onChange={this.onChangeDestinationAddress} onChange={this.onChangeDestinationAddress}
placeholder={donationAddressMap[destinationKind]} placeholder={donationAddressMap[destinationId]}
/> />
</label> </label>
</div> </div>

View File

@ -1,4 +1,5 @@
import { RestartSwapAction } from 'actions/swap'; import { RestartSwapAction } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate from 'translations'; import translate from 'translations';
import { toFixedIfLarger } from 'utils/formatters'; import { toFixedIfLarger } from 'utils/formatters';
@ -6,10 +7,8 @@ import './SwapInfoHeader.scss';
import SwapInfoHeaderTitle from './SwapInfoHeaderTitle'; import SwapInfoHeaderTitle from './SwapInfoHeaderTitle';
export interface SwapInfoHeaderProps { export interface SwapInfoHeaderProps {
originAmount: number | null; origin: SwapInput;
originKind: string; destination: SwapInput;
destinationKind: string;
destinationAmount: number | null;
reference: string; reference: string;
secondsRemaining: number | null; secondsRemaining: number | null;
restartSwap(): RestartSwapAction; restartSwap(): RestartSwapAction;
@ -17,10 +16,11 @@ export interface SwapInfoHeaderProps {
export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> { export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
public computedOriginDestinationRatio = () => { public computedOriginDestinationRatio = () => {
if (!this.props.originAmount || !this.props.destinationAmount) { const { origin, destination } = this.props;
if (!origin.amount || !destination.amount) {
return; return;
} }
return this.props.destinationAmount / this.props.originAmount; return destination.amount / origin.amount;
}; };
public isExpanded = () => { public isExpanded = () => {
@ -51,14 +51,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
public render() { public render() {
const computedOriginDestinationRatio = this.computedOriginDestinationRatio(); const computedOriginDestinationRatio = this.computedOriginDestinationRatio();
const { const { reference, origin, destination, restartSwap } = this.props;
reference,
originAmount,
destinationAmount,
originKind,
destinationKind,
restartSwap
} = this.props;
return ( return (
<div className="SwapInfo"> <div className="SwapInfo">
<SwapInfoHeaderTitle restartSwap={restartSwap} /> <SwapInfoHeaderTitle restartSwap={restartSwap} />
@ -66,12 +59,8 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Amount to send*/} {/*Amount to send*/}
{!this.isExpanded() && ( {!this.isExpanded() && (
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{` ${originAmount} ${ <h3 className="SwapInfo-details-block-value">{` ${origin.amount} ${origin.id}`}</h3>
originKind <p className="SwapInfo-details-block-label">{translate('SEND_amount')}</p>
}`}</h3>
<p className="SwapInfo-details-block-label">
{translate('SEND_amount')}
</p>
</div> </div>
)} )}
@ -79,45 +68,33 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{this.isExpanded() && ( {this.isExpanded() && (
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{reference}</h3> <h3 className="SwapInfo-details-block-value">{reference}</h3>
<p className="SwapInfo-details-block-label"> <p className="SwapInfo-details-block-label">{translate('SWAP_ref_num')}</p>
{translate('SWAP_ref_num')}
</p>
</div> </div>
)} )}
{/*Time remaining*/} {/*Time remaining*/}
{this.isExpanded() && ( {this.isExpanded() && (
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value"> <h3 className="SwapInfo-details-block-value">{this.formattedTime()}</h3>
{this.formattedTime()} <p className="SwapInfo-details-block-label">{translate('SWAP_time')}</p>
</h3>
<p className="SwapInfo-details-block-label">
{translate('SWAP_time')}
</p>
</div> </div>
)} )}
{/*Amount to Receive*/} {/*Amount to Receive*/}
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value"> <h3 className="SwapInfo-details-block-value">
{` ${destinationAmount} ${destinationKind}`} {` ${destination.amount} ${destination.id}`}
</h3> </h3>
<p className="SwapInfo-details-block-label"> <p className="SwapInfo-details-block-label">{translate('SWAP_rec_amt')}</p>
{translate('SWAP_rec_amt')}
</p>
</div> </div>
{/*Your rate*/} {/*Your rate*/}
<div className={this.computedClass()}> <div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value"> <h3 className="SwapInfo-details-block-value">
{`${computedOriginDestinationRatio && {`${computedOriginDestinationRatio &&
toFixedIfLarger(computedOriginDestinationRatio)} ${ toFixedIfLarger(computedOriginDestinationRatio)} ${destination.id}/${origin.id}`}
destinationKind
}/${originKind}`}
</h3> </h3>
<p className="SwapInfo-details-block-label"> <p className="SwapInfo-details-block-label">{translate('SWAP_your_rate')}</p>
{translate('SWAP_your_rate')}
</p>
</div> </div>
</section> </section>
</div> </div>

View File

@ -5,10 +5,10 @@ import translate, { translateRaw } from 'translations';
import './SwapProgress.scss'; import './SwapProgress.scss';
export interface Props { export interface Props {
destinationKind: string; destinationId: string;
originId: string;
destinationAddress: string; destinationAddress: string;
outputTx: string; outputTx: string;
originKind: string;
orderStatus: string | null; orderStatus: string | null;
// actions // actions
showNotification: TShowNotification; showNotification: TShowNotification;
@ -28,12 +28,7 @@ export default class SwapProgress extends Component<Props, State> {
public showSwapNotification = () => { public showSwapNotification = () => {
const { hasShownViewTx } = this.state; const { hasShownViewTx } = this.state;
const { const { destinationId, outputTx, showNotification, orderStatus } = this.props;
destinationKind,
outputTx,
showNotification,
orderStatus
} = this.props;
if (orderStatus === 'FILL') { if (orderStatus === 'FILL') {
if (!hasShownViewTx) { if (!hasShownViewTx) {
@ -41,7 +36,7 @@ export default class SwapProgress extends Component<Props, State> {
let link; let link;
const notificationMessage = translateRaw('SUCCESS_3') + outputTx; const notificationMessage = translateRaw('SUCCESS_3') + outputTx;
// everything but BTC is a token // everything but BTC is a token
if (destinationKind !== 'BTC') { if (destinationId !== 'BTC') {
link = bityConfig.ETHTxExplorer(outputTx); link = bityConfig.ETHTxExplorer(outputTx);
linkElement = ( linkElement = (
<a href={link} target="_blank" rel="noopener"> <a href={link} target="_blank" rel="noopener">
@ -97,22 +92,22 @@ export default class SwapProgress extends Component<Props, State> {
}; };
public render() { public render() {
const { destinationKind, originKind } = this.props; const { originId, destinationId } = this.props;
const numberOfConfirmations = originKind === 'BTC' ? '3' : '10'; const numberOfConfirmations = originId === 'BTC' ? '3' : '10';
const steps = [ const steps = [
// 1 // 1
translate('SWAP_progress_1'), translate('SWAP_progress_1'),
// 2 // 2
<span key="1"> <span key="1">
{translate('SWAP_progress_2')} {originKind}... {translate('SWAP_progress_2')} {originId}...
</span>, </span>,
// 3 // 3
<span key="2"> <span key="2">
{originKind} {translate('SWAP_progress_3')} {originId} {translate('SWAP_progress_3')}
</span>, </span>,
// 4 TODO: Translate me // 4 TODO: Translate me
<span key="3"> <span key="3">
Sending your {destinationKind} Sending your {destinationId}
<br /> <br />
<small>Waiting for {numberOfConfirmations} confirmations...</small> <small>Waiting for {numberOfConfirmations} confirmations...</small>
</span>, </span>,
@ -128,9 +123,7 @@ export default class SwapProgress extends Component<Props, State> {
return ( return (
<div key={idx} className={this.computedClass(idx + 1)}> <div key={idx} className={this.computedClass(idx + 1)}>
<div className={`SwapProgress-item-circle position-${idx + 1}`}> <div className={`SwapProgress-item-circle position-${idx + 1}`}>
<span className="SwapProgress-item-circle-number"> <span className="SwapProgress-item-circle-number">{idx + 1}</span>
{idx + 1}
</span>
</div> </div>
<p className="SwapProgress-item-text">{text}</p> <p className="SwapProgress-item-text">{text}</p>
</div> </div>

View File

@ -1,30 +1,21 @@
import { showNotification as dShowNotification, TShowNotification } from 'actions/notifications';
import { import {
showNotification as dShowNotification, initSwap as dInitSwap,
TShowNotification
} from 'actions/notifications';
import {
bityOrderCreateRequestedSwap as dBityOrderCreateRequestedSwap, bityOrderCreateRequestedSwap as dBityOrderCreateRequestedSwap,
changeStepSwap as dChangeStepSwap, changeStepSwap as dChangeStepSwap,
destinationAddressSwap as dDestinationAddressSwap, destinationAddressSwap as dDestinationAddressSwap,
destinationAmountSwap as dDestinationAmountSwap,
destinationKindSwap as dDestinationKindSwap,
loadBityRatesRequestedSwap as dLoadBityRatesRequestedSwap, loadBityRatesRequestedSwap as dLoadBityRatesRequestedSwap,
originAmountSwap as dOriginAmountSwap,
originKindSwap as dOriginKindSwap,
restartSwap as dRestartSwap, restartSwap as dRestartSwap,
startOrderTimerSwap as dStartOrderTimerSwap, startOrderTimerSwap as dStartOrderTimerSwap,
startPollBityOrderStatus as dStartPollBityOrderStatus, startPollBityOrderStatus as dStartPollBityOrderStatus,
stopLoadBityRatesSwap as dStopLoadBityRatesSwap, stopLoadBityRatesSwap as dStopLoadBityRatesSwap,
stopOrderTimerSwap as dStopOrderTimerSwap, stopOrderTimerSwap as dStopOrderTimerSwap,
stopPollBityOrderStatus as dStopPollBityOrderStatus, stopPollBityOrderStatus as dStopPollBityOrderStatus,
TInitSwap,
TBityOrderCreateRequestedSwap, TBityOrderCreateRequestedSwap,
TChangeStepSwap, TChangeStepSwap,
TDestinationAddressSwap, TDestinationAddressSwap,
TDestinationAmountSwap,
TDestinationKindSwap,
TLoadBityRatesRequestedSwap, TLoadBityRatesRequestedSwap,
TOriginAmountSwap,
TOriginKindSwap,
TRestartSwap, TRestartSwap,
TStartOrderTimerSwap, TStartOrderTimerSwap,
TStartPollBityOrderStatus, TStartPollBityOrderStatus,
@ -32,6 +23,7 @@ import {
TStopOrderTimerSwap, TStopOrderTimerSwap,
TStopPollBityOrderStatus TStopPollBityOrderStatus
} from 'actions/swap'; } from 'actions/swap';
import { SwapInput, NormalizedOptions, NormalizedBityRates } from 'reducers/swap/types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
@ -43,14 +35,11 @@ import SwapInfoHeader from './components/SwapInfoHeader';
import TabSection from 'containers/TabSection'; import TabSection from 'containers/TabSection';
interface ReduxStateProps { interface ReduxStateProps {
originAmount: number | null;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
step: number; step: number;
bityRates: any; origin: SwapInput;
destination: SwapInput;
bityRates: NormalizedBityRates;
options: NormalizedOptions;
bityOrder: any; bityOrder: any;
destinationAddress: string; destinationAddress: string;
isFetchingRates: boolean | null; isFetchingRates: boolean | null;
@ -63,10 +52,6 @@ interface ReduxStateProps {
interface ReduxActionProps { interface ReduxActionProps {
changeStepSwap: TChangeStepSwap; changeStepSwap: TChangeStepSwap;
originKindSwap: TOriginKindSwap;
destinationKindSwap: TDestinationKindSwap;
originAmountSwap: TOriginAmountSwap;
destinationAmountSwap: TDestinationAmountSwap;
loadBityRatesRequestedSwap: TLoadBityRatesRequestedSwap; loadBityRatesRequestedSwap: TLoadBityRatesRequestedSwap;
destinationAddressSwap: TDestinationAddressSwap; destinationAddressSwap: TDestinationAddressSwap;
restartSwap: TRestartSwap; restartSwap: TRestartSwap;
@ -77,11 +62,11 @@ interface ReduxActionProps {
stopPollBityOrderStatus: TStopPollBityOrderStatus; stopPollBityOrderStatus: TStopPollBityOrderStatus;
showNotification: TShowNotification; showNotification: TShowNotification;
startOrderTimerSwap: TStartOrderTimerSwap; startOrderTimerSwap: TStartOrderTimerSwap;
initSwap: TInitSwap;
} }
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> { class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() { public componentDidMount() {
// TODO: Use `isFetchingRates` to show a loader
this.props.loadBityRatesRequestedSwap(); this.props.loadBityRatesRequestedSwap();
} }
@ -93,12 +78,9 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
const { const {
// STATE // STATE
bityRates, bityRates,
originAmount, options,
destinationAmount, origin,
originKind, destination,
destinationKind,
destinationKindOptions,
originKindOptions,
destinationAddress, destinationAddress,
step, step,
bityOrder, bityOrder,
@ -108,13 +90,10 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
isPostingOrder, isPostingOrder,
outputTx, outputTx,
// ACTIONS // ACTIONS
initSwap,
restartSwap, restartSwap,
stopLoadBityRatesSwap, stopLoadBityRatesSwap,
changeStepSwap, changeStepSwap,
originKindSwap,
destinationKindSwap,
originAmountSwap,
destinationAmountSwap,
destinationAddressSwap, destinationAddressSwap,
bityOrderCreateRequestedSwap, bityOrderCreateRequestedSwap,
showNotification, showNotification,
@ -128,9 +107,8 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
const ReceivingAddressProps = { const ReceivingAddressProps = {
isPostingOrder, isPostingOrder,
originAmount, origin,
originKind, destinationId: destination.id,
destinationKind,
destinationAddressSwap, destinationAddressSwap,
destinationAddress, destinationAddress,
stopLoadBityRatesSwap, stopLoadBityRatesSwap,
@ -139,38 +117,24 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
}; };
const SwapInfoHeaderProps = { const SwapInfoHeaderProps = {
origin,
destination,
reference, reference,
secondsRemaining, secondsRemaining,
originAmount,
originKind,
destinationKind,
destinationAmount,
restartSwap, restartSwap,
orderStatus orderStatus
}; };
const { ETHBTC, ETHREP, BTCETH, BTCREP } = bityRates;
const CurrentRatesProps = { ETHBTC, ETHREP, BTCETH, BTCREP };
const CurrencySwapProps = { const CurrencySwapProps = {
showNotification, showNotification,
bityRates, bityRates,
originAmount, options,
destinationAmount, initSwap,
originKind,
destinationKind,
destinationKindOptions,
originKindOptions,
originKindSwap,
destinationKindSwap,
originAmountSwap,
destinationAmountSwap,
changeStepSwap changeStepSwap
}; };
const PaymentInfoProps = { const PaymentInfoProps = {
originKind, origin,
originAmount,
paymentAddress paymentAddress
}; };
@ -187,13 +151,14 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
outputTx outputTx
}; };
const { ETHBTC, ETHREP, BTCETH, BTCREP } = bityRates.byId;
const CurrentRatesProps = { ETHBTC, ETHREP, BTCETH, BTCREP };
return ( return (
<TabSection> <TabSection>
<section className="Tab-content swap-tab"> <section className="Tab-content swap-tab">
{step === 1 && <CurrentRates {...CurrentRatesProps} />} {step === 1 && <CurrentRates {...CurrentRatesProps} />}
{(step === 2 || step === 3) && ( {(step === 2 || step === 3) && <SwapInfoHeader {...SwapInfoHeaderProps} />}
<SwapInfoHeader {...SwapInfoHeaderProps} />
)}
<main className="Tab-content-pane"> <main className="Tab-content-pane">
{step === 1 && <CurrencySwap {...CurrencySwapProps} />} {step === 1 && <CurrencySwap {...CurrencySwapProps} />}
@ -208,14 +173,11 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
originAmount: state.swap.originAmount,
destinationAmount: state.swap.destinationAmount,
originKind: state.swap.originKind,
destinationKind: state.swap.destinationKind,
destinationKindOptions: state.swap.destinationKindOptions,
originKindOptions: state.swap.originKindOptions,
step: state.swap.step, step: state.swap.step,
origin: state.swap.origin,
destination: state.swap.destination,
bityRates: state.swap.bityRates, bityRates: state.swap.bityRates,
options: state.swap.options,
bityOrder: state.swap.bityOrder, bityOrder: state.swap.bityOrder,
destinationAddress: state.swap.destinationAddress, destinationAddress: state.swap.destinationAddress,
isFetchingRates: state.swap.isFetchingRates, isFetchingRates: state.swap.isFetchingRates,
@ -228,14 +190,11 @@ function mapStateToProps(state: AppState) {
} }
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
changeStepSwap: dChangeStepSwap, changeStepSwap: dChangeStepSwap,
destinationAddressSwap: dDestinationAddressSwap, initSwap: dInitSwap,
destinationAmountSwap: dDestinationAmountSwap, bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
destinationKindSwap: dDestinationKindSwap,
loadBityRatesRequestedSwap: dLoadBityRatesRequestedSwap, loadBityRatesRequestedSwap: dLoadBityRatesRequestedSwap,
originAmountSwap: dOriginAmountSwap, destinationAddressSwap: dDestinationAddressSwap,
originKindSwap: dOriginKindSwap,
restartSwap: dRestartSwap, restartSwap: dRestartSwap,
startOrderTimerSwap: dStartOrderTimerSwap, startOrderTimerSwap: dStartOrderTimerSwap,
startPollBityOrderStatus: dStartPollBityOrderStatus, startPollBityOrderStatus: dStartPollBityOrderStatus,

View File

@ -1,36 +0,0 @@
import without from 'lodash/without';
import { combineAndUpper } from 'utils/formatters';
import { ALL_CRYPTO_KIND_OPTIONS } from '.';
export const buildDestinationAmount = (
originAmount,
originKind,
destinationKind,
bityRates
) => {
const pairName = combineAndUpper(originKind, destinationKind);
const bityRate = bityRates[pairName];
return originAmount !== null ? originAmount * bityRate : null;
};
export const buildDestinationKind = (
originKind: string,
destinationKind: string
): string => {
if (originKind === destinationKind) {
return without(ALL_CRYPTO_KIND_OPTIONS, originKind)[0];
} else {
return destinationKind;
}
};
export const buildOriginKind = (
originKind: string,
destinationKind: string
): string => {
if (originKind === destinationKind) {
return without(ALL_CRYPTO_KIND_OPTIONS, destinationKind)[0];
} else {
return originKind;
}
};

View File

@ -1,24 +1,15 @@
import * as actionTypes from 'actions/swap'; import * as actionTypes from 'actions/swap';
import * as stateTypes from './types';
import * as schema from './schema';
import { TypeKeys } from 'actions/swap/constants'; import { TypeKeys } from 'actions/swap/constants';
import without from 'lodash/without'; import { normalize } from 'normalizr';
import {
buildDestinationAmount,
buildDestinationKind,
buildOriginKind
} from './helpers';
export const ALL_CRYPTO_KIND_OPTIONS = ['BTC', 'ETH', 'REP'];
const DEFAULT_ORIGIN_KIND = 'BTC';
const DEFAULT_DESTINATION_KIND = 'ETH';
export interface State { export interface State {
originAmount: number | null;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
step: number; step: number;
bityRates: any; origin: stateTypes.SwapInput;
destination: stateTypes.SwapInput;
options: stateTypes.NormalizedOptions;
bityRates: stateTypes.NormalizedBityRates;
bityOrder: any; bityOrder: any;
destinationAddress: string; destinationAddress: string;
isFetchingRates: boolean | null; isFetchingRates: boolean | null;
@ -33,14 +24,17 @@ export interface State {
} }
export const INITIAL_STATE: State = { export const INITIAL_STATE: State = {
originAmount: null,
destinationAmount: null,
originKind: DEFAULT_ORIGIN_KIND,
destinationKind: DEFAULT_DESTINATION_KIND,
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, DEFAULT_ORIGIN_KIND),
originKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, 'REP'),
step: 1, step: 1,
bityRates: {}, origin: { id: 'BTC', amount: NaN },
destination: { id: 'ETH', amount: NaN },
options: {
byId: {},
allIds: []
},
bityRates: {
byId: {},
allIds: []
},
destinationAddress: '', destinationAddress: '',
bityOrder: {}, bityOrder: {},
isFetchingRates: null, isFetchingRates: null,
@ -54,76 +48,29 @@ export const INITIAL_STATE: State = {
orderId: null orderId: null
}; };
function handleSwapOriginKind( export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapAction) {
state: State,
action: actionTypes.OriginKindSwapAction
) {
const newDestinationKind = buildDestinationKind(
action.payload,
state.destinationKind
);
return {
...state,
originKind: action.payload,
destinationKind: newDestinationKind,
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, action.payload),
destinationAmount: buildDestinationAmount(
state.originAmount,
action.payload,
newDestinationKind,
state.bityRates
)
};
}
function handleSwapDestinationKind(
state: State,
action: actionTypes.DestinationKindSwapAction
) {
const newOriginKind = buildOriginKind(state.originKind, action.payload);
return {
...state,
originKind: newOriginKind,
destinationKind: action.payload,
destinationAmount: buildDestinationAmount(
state.originAmount,
state.originKind,
action.payload,
state.bityRates
)
};
}
export function swap(
state: State = INITIAL_STATE,
action: actionTypes.SwapAction
) {
switch (action.type) { switch (action.type) {
case TypeKeys.SWAP_ORIGIN_KIND: {
return handleSwapOriginKind(state, action);
}
case TypeKeys.SWAP_DESTINATION_KIND: {
return handleSwapDestinationKind(state, action);
}
case TypeKeys.SWAP_ORIGIN_AMOUNT:
return {
...state,
originAmount: action.payload
};
case TypeKeys.SWAP_DESTINATION_AMOUNT:
return {
...state,
destinationAmount: action.payload
};
case TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED: case TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED:
const { payload } = action;
return { return {
...state, ...state,
bityRates: { bityRates: {
...state.bityRates, byId: normalize(payload, [schema.bityRate]).entities.bityRates,
...action.payload allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.bityRates)
},
options: {
byId: normalize(payload, [schema.bityRate]).entities.options,
allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.options)
}, },
isFetchingRates: false isFetchingRates: false
}; };
case TypeKeys.SWAP_INIT: {
return {
...state,
origin: action.payload.origin,
destination: action.payload.destination
};
}
case TypeKeys.SWAP_STEP: { case TypeKeys.SWAP_STEP: {
return { return {
...state, ...state,

View File

@ -0,0 +1,9 @@
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', {
options: [option]
});

View File

@ -0,0 +1,23 @@
import { Option } from 'actions/swap/actionTypes';
import { WhitelistedCoins } from 'config/bity';
export interface SwapInput {
id: WhitelistedCoins;
amount: number;
}
export interface NormalizedBityRate {
id: number;
options: WhitelistedCoins[];
rate: number;
}
export interface NormalizedBityRates {
byId: { [id: string]: NormalizedBityRate };
allIds: string[];
}
export interface NormalizedOptions {
byId: { [id: string]: Option };
allIds: string[];
}

View File

@ -1,17 +1,17 @@
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { routerMiddleware } from 'react-router-redux'; import { routerMiddleware } from 'react-router-redux';
import { INITIAL_STATE as configInitialState } from 'reducers/config'; import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
import { INITIAL_STATE as customTokensInitialState } from 'reducers/customTokens'; import {
import { INITIAL_STATE as swapInitialState } from 'reducers/swap'; State as CustomTokenState,
INITIAL_STATE as customTokensInitialState
} from 'reducers/customTokens';
import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
import { applyMiddleware, createStore } from 'redux'; import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'; import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger'; import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
import RootReducer from './reducers'; import RootReducer from './reducers';
import { State as ConfigState } from './reducers/config';
import { State as CustomTokenState } from './reducers/customTokens';
import { State as SwapState } from './reducers/swap';
import promiseMiddleware from 'redux-promise-middleware'; import promiseMiddleware from 'redux-promise-middleware';
import { getNodeConfigFromId } from 'utils/node'; import { getNodeConfigFromId } from 'utils/node';
@ -54,13 +54,9 @@ const configureStore = () => {
} }
: { ...swapInitialState }; : { ...swapInitialState };
const localCustomTokens = loadStatePropertyOrEmptyObject<CustomTokenState>( const localCustomTokens = loadStatePropertyOrEmptyObject<CustomTokenState>('customTokens');
'customTokens'
);
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>( const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
'config'
);
// If they have a saved node, make sure we assign that too. The node selected // If they have a saved node, make sure we assign that too. The node selected
// isn't serializable, so we have to assign it here. // isn't serializable, so we have to assign it here.
@ -90,8 +86,7 @@ const configureStore = () => {
// if 'web3' has persisted as node selection, reset to app default // if 'web3' has persisted as node selection, reset to app default
// necessary because web3 is only initialized as a node upon MetaMask / Mist unlock // necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
if (persistedInitialState.config.nodeSelection === 'web3') { if (persistedInitialState.config.nodeSelection === 'web3') {
persistedInitialState.config.nodeSelection = persistedInitialState.config.nodeSelection = configInitialState.nodeSelection;
configInitialState.nodeSelection;
} }
store = createStore(RootReducer, persistedInitialState, middleware); store = createStore(RootReducer, persistedInitialState, middleware);
@ -112,7 +107,11 @@ const configureStore = () => {
customNodes: state.config.customNodes, customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks customNetworks: state.config.customNetworks
}, },
swap: { ...state.swap, bityRates: {} }, swap: {
...state.swap,
options: {},
bityRates: {}
},
customTokens: state.customTokens customTokens: state.customTokens
}); });
}), }),

View File

@ -23,6 +23,7 @@
"jsonschema": "1.2.0", "jsonschema": "1.2.0",
"lodash": "4.17.4", "lodash": "4.17.4",
"moment": "2.19.3", "moment": "2.19.3",
"normalizr": "3.2.4",
"qrcode": "1.0.0", "qrcode": "1.0.0",
"qrcode.react": "0.7.2", "qrcode.react": "0.7.2",
"query-string": "5.0.1", "query-string": "5.0.1",

View File

@ -4,31 +4,12 @@ import Adapter from 'enzyme-adapter-react-16';
import Swap from 'containers/Tabs/Swap'; import Swap from 'containers/Tabs/Swap';
import shallowWithStore from '../utils/shallowWithStore'; import shallowWithStore from '../utils/shallowWithStore';
import { createMockStore } from 'redux-test-utils'; import { createMockStore } from 'redux-test-utils';
import { INITIAL_STATE } from 'reducers/swap';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
it('render snapshot', () => { it('render snapshot', () => {
const testState = { const store = createMockStore({ swap: INITIAL_STATE });
swap: {
originAmount: {},
destinationAmount: {},
originKind: {},
destinationKind: {},
destinationKindOptions: {},
originKindOptions: {},
step: {},
bityRates: {},
bityOrder: {},
destinationAddress: {},
isFetchingRates: {},
secondsRemaining: {},
outputTx: {},
isPostingOrder: {},
orderStatus: {},
paymentAddress: {}
}
};
const store = createMockStore(testState);
const component = shallowWithStore(<Swap />, store); const component = shallowWithStore(<Swap />, store);
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();

View File

@ -4,32 +4,46 @@ exports[`render snapshot 1`] = `
<Swap <Swap
bityOrder={Object {}} bityOrder={Object {}}
bityOrderCreateRequestedSwap={[Function]} bityOrderCreateRequestedSwap={[Function]}
bityRates={Object {}} bityRates={
Object {
"allIds": Array [],
"byId": Object {},
}
}
changeStepSwap={[Function]} changeStepSwap={[Function]}
destinationAddress={Object {}} destination={
Object {
"amount": NaN,
"id": "ETH",
}
}
destinationAddress=""
destinationAddressSwap={[Function]} destinationAddressSwap={[Function]}
destinationAmount={Object {}} initSwap={[Function]}
destinationAmountSwap={[Function]} isFetchingRates={null}
destinationKind={Object {}} isPostingOrder={false}
destinationKindOptions={Object {}}
destinationKindSwap={[Function]}
isFetchingRates={Object {}}
isPostingOrder={Object {}}
loadBityRatesRequestedSwap={[Function]} loadBityRatesRequestedSwap={[Function]}
orderStatus={Object {}} options={
originAmount={Object {}} Object {
originAmountSwap={[Function]} "allIds": Array [],
originKind={Object {}} "byId": Object {},
originKindOptions={Object {}} }
originKindSwap={[Function]} }
outputTx={Object {}} orderStatus={null}
paymentAddress={Object {}} origin={
Object {
"amount": NaN,
"id": "BTC",
}
}
outputTx={null}
paymentAddress={null}
restartSwap={[Function]} restartSwap={[Function]}
secondsRemaining={Object {}} secondsRemaining={null}
showNotification={[Function]} showNotification={[Function]}
startOrderTimerSwap={[Function]} startOrderTimerSwap={[Function]}
startPollBityOrderStatus={[Function]} startPollBityOrderStatus={[Function]}
step={Object {}} step={1}
stopLoadBityRatesSwap={[Function]} stopLoadBityRatesSwap={[Function]}
stopOrderTimerSwap={[Function]} stopOrderTimerSwap={[Function]}
stopPollBityOrderStatus={[Function]} stopPollBityOrderStatus={[Function]}

View File

@ -1,95 +1,36 @@
import { swap, INITIAL_STATE, ALL_CRYPTO_KIND_OPTIONS } from 'reducers/swap'; import { swap, INITIAL_STATE } from 'reducers/swap';
import {
buildDestinationAmount,
buildDestinationKind,
buildOriginKind
} from 'reducers/swap/helpers';
import * as swapActions from 'actions/swap'; import * as swapActions from 'actions/swap';
import without from 'lodash/without'; import { NormalizedBityRates, NormalizedOptions } from 'reducers/swap/types';
import { normalize } from 'normalizr';
import * as schema from 'reducers/swap/schema';
describe('swap reducer', () => { describe('swap reducer', () => {
it('should handle SWAP_ORIGIN_KIND', () => { const apiResponse = {
const newOriginKind = 'ETH'; BTCETH: {
const newDestinationKind = buildDestinationKind( id: 'BTCETH',
newOriginKind, options: [{ id: 'BTC' }, { id: 'ETH' }],
INITIAL_STATE.destinationKind rate: 23.27855114
); },
const fakeBityRates = { ETHBTC: {
BTCETH: 10, id: 'ETHBTC',
ETHBTC: 0.01 options: [{ id: 'ETH' }, { id: 'BTC' }],
}; rate: 0.042958
expect(swap(undefined, swapActions.originKindSwap(newOriginKind))).toEqual({
...INITIAL_STATE,
originKind: newOriginKind,
destinationKind: newDestinationKind,
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, newOriginKind),
destinationAmount: buildDestinationAmount(
INITIAL_STATE.originAmount,
newOriginKind,
newDestinationKind,
fakeBityRates
)
});
});
it('should handle SWAP_DESTINATION_KIND', () => {
const newDestinationKind = 'REP';
const newOriginKind = buildOriginKind(
INITIAL_STATE.originKind,
newDestinationKind
);
const fakeBityRates = {
BTCETH: 10,
ETHBTC: 0.01
};
expect(
swap(undefined, swapActions.destinationKindSwap(newDestinationKind))
).toEqual({
...INITIAL_STATE,
destinationKind: newDestinationKind,
destinationKindOptions: without(ALL_CRYPTO_KIND_OPTIONS, newOriginKind),
destinationAmount: buildDestinationAmount(
INITIAL_STATE.originAmount,
newOriginKind,
newDestinationKind,
fakeBityRates
)
});
});
it('should handle SWAP_ORIGIN_AMOUNT', () => {
const originAmount = 2;
expect(swap(undefined, swapActions.originAmountSwap(originAmount))).toEqual(
{
...INITIAL_STATE,
originAmount
} }
);
});
it('should handle SWAP_DESTINATION_AMOUNT', () => {
const destinationAmount = 2;
expect(
swap(undefined, swapActions.destinationAmountSwap(destinationAmount))
).toEqual({
...INITIAL_STATE,
destinationAmount
});
});
it('should handle SWAP_LOAD_BITY_RATES_SUCCEEDED', () => {
const bityRates = {
BTCETH: 0.01,
ETHREP: 10,
ETHBTC: 0,
BTCREP: 0
}; };
expect( const normalizedbityRates: NormalizedBityRates = {
swap(undefined, swapActions.loadBityRatesSucceededSwap(bityRates)) byId: normalize(apiResponse, [schema.bityRate]).entities.bityRates,
).toEqual({ allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.bityRates)
};
const normalizedOptions: NormalizedOptions = {
byId: normalize(apiResponse, [schema.bityRate]).entities.options,
allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.options)
};
it('should handle SWAP_LOAD_BITY_RATES_SUCCEEDED', () => {
expect(swap(undefined, swapActions.loadBityRatesSucceededSwap(apiResponse))).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
isFetchingRates: false, isFetchingRates: false,
bityRates bityRates: normalizedbityRates,
options: normalizedOptions
}); });
}); });
@ -103,31 +44,26 @@ describe('swap reducer', () => {
it('should handle SWAP_DESTINATION_ADDRESS', () => { it('should handle SWAP_DESTINATION_ADDRESS', () => {
const destinationAddress = '341a0sdf83'; const destinationAddress = '341a0sdf83';
expect( expect(swap(undefined, swapActions.destinationAddressSwap(destinationAddress))).toEqual({
swap(undefined, swapActions.destinationAddressSwap(destinationAddress))
).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
destinationAddress destinationAddress
}); });
}); });
it('should handle SWAP_RESTART', () => { it('should handle SWAP_RESTART', () => {
const bityRates = {
BTCETH: 0.01,
ETHREP: 10
};
expect( expect(
swap( swap(
{ {
...INITIAL_STATE, ...INITIAL_STATE,
bityRates, bityRates: normalizedbityRates,
originAmount: 1 origin: { id: 'BTC', amount: 1 },
destination: { id: 'ETH', amount: 3 }
}, },
swapActions.restartSwap() swapActions.restartSwap()
) )
).toEqual({ ).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
bityRates bityRates: normalizedbityRates
}); });
}); });
@ -174,9 +110,7 @@ describe('swap reducer', () => {
id: 'id' id: 'id'
}; };
expect( expect(swap(undefined, swapActions.bityOrderCreateSucceededSwap(mockedBityOrder))).toEqual({
swap(undefined, swapActions.bityOrderCreateSucceededSwap(mockedBityOrder))
).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
bityOrder: { bityOrder: {
...mockedBityOrder ...mockedBityOrder
@ -210,9 +144,7 @@ describe('swap reducer', () => {
status: 'status' status: 'status'
}; };
expect( expect(swap(undefined, swapActions.orderStatusSucceededSwap(mockedBityResponse))).toEqual({
swap(undefined, swapActions.orderStatusSucceededSwap(mockedBityResponse))
).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
outputTx: mockedBityResponse.output.reference, outputTx: mockedBityResponse.output.reference,
orderStatus: mockedBityResponse.output.status orderStatus: mockedBityResponse.output.status
@ -221,9 +153,7 @@ describe('swap reducer', () => {
it('should handle SWAP_ORDER_TIME', () => { it('should handle SWAP_ORDER_TIME', () => {
const secondsRemaining = 300; const secondsRemaining = 300;
expect( expect(swap(undefined, swapActions.orderTimeSwap(secondsRemaining))).toEqual({
swap(undefined, swapActions.orderTimeSwap(secondsRemaining))
).toEqual({
...INITIAL_STATE, ...INITIAL_STATE,
secondsRemaining secondsRemaining
}); });

View File

@ -4,21 +4,22 @@ import { getAllRates } from 'api/bity';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects'; import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
import { createMockTask } from 'redux-saga/utils'; import { createMockTask } from 'redux-saga/utils';
import { Pairs } from 'actions/swap/actionTypes'; import { loadBityRates, handleBityRates, getBityRatesSaga } from 'sagas/swap/rates';
import {
loadBityRates,
handleBityRates,
getBityRatesSaga
} from 'sagas/swap/rates';
describe('loadBityRates*', () => { describe('loadBityRates*', () => {
const gen1 = loadBityRates(); const gen1 = loadBityRates();
const gen2 = loadBityRates(); const gen2 = loadBityRates();
const rates: Pairs = { const apiResponse = {
ETHBTC: 1, BTCETH: {
ETHREP: 2, id: 'BTCETH',
BTCETH: 3, options: [{ id: 'BTC' }, { id: 'ETH' }],
BTCREP: 4 rate: 23.27855114
},
ETHBTC: {
id: 'ETHBTC',
options: [{ id: 'ETH' }, { id: 'BTC' }],
rate: 0.042958
}
}; };
let random; let random;
@ -36,9 +37,7 @@ describe('loadBityRates*', () => {
}); });
it('should put loadBityRatesSucceededSwap', () => { it('should put loadBityRatesSucceededSwap', () => {
expect(gen1.next(rates).value).toEqual( expect(gen1.next(apiResponse).value).toEqual(put(loadBityRatesSucceededSwap(apiResponse)));
put(loadBityRatesSucceededSwap(rates))
);
}); });
it('should call delay for 5 seconds', () => { it('should call delay for 5 seconds', () => {
@ -48,9 +47,7 @@ describe('loadBityRates*', () => {
it('should handle an exception', () => { it('should handle an exception', () => {
const err = { message: 'error' }; const err = { message: 'error' };
gen2.next(); gen2.next();
expect((gen2 as any).throw(err).value).toEqual( expect((gen2 as any).throw(err).value).toEqual(put(showNotification('danger', err.message)));
put(showNotification('danger', err.message))
);
}); });
}); });
@ -79,8 +76,6 @@ describe('getBityRatesSaga*', () => {
const gen = getBityRatesSaga(); const gen = getBityRatesSaga();
it('should takeLatest SWAP_LOAD_RATES_REQUESTED', () => { it('should takeLatest SWAP_LOAD_RATES_REQUESTED', () => {
expect(gen.next().value).toEqual( expect(gen.next().value).toEqual(takeLatest('SWAP_LOAD_BITY_RATES_REQUESTED', handleBityRates));
takeLatest('SWAP_LOAD_BITY_RATES_REQUESTED', handleBityRates)
);
}); });
}); });