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';
export type TChangeStepSwap = typeof changeStepSwap;
export function changeStepSwap(
payload: number
): interfaces.ChangeStepSwapAction {
export function changeStepSwap(payload: number): interfaces.ChangeStepSwapAction {
return {
type: TypeKeys.SWAP_STEP,
payload
};
}
export type TOriginKindSwap = typeof originKindSwap;
export function originKindSwap(
payload: string
): interfaces.OriginKindSwapAction {
export type TInitSwap = typeof initSwap;
export function initSwap(payload: interfaces.SwapInputs): interfaces.InitSwap {
return {
type: TypeKeys.SWAP_ORIGIN_KIND,
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,
type: TypeKeys.SWAP_INIT,
payload
};
}
export type TLoadBityRatesSucceededSwap = typeof loadBityRatesSucceededSwap;
export function loadBityRatesSucceededSwap(
payload: interfaces.Pairs
payload: interfaces.ApiResponse
): interfaces.LoadBityRatesSucceededSwapAction {
return {
type: TypeKeys.SWAP_LOAD_BITY_RATES_SUCCEEDED,
@ -62,9 +28,7 @@ export function loadBityRatesSucceededSwap(
}
export type TDestinationAddressSwap = typeof destinationAddressSwap;
export function destinationAddressSwap(
payload?: string
): interfaces.DestinationAddressSwapAction {
export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction {
return {
type: TypeKeys.SWAP_DESTINATION_ADDRESS,
payload
@ -93,9 +57,7 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction
}
export type TOrderTimeSwap = typeof orderTimeSwap;
export function orderTimeSwap(
payload: number
): interfaces.OrderSwapTimeSwapAction {
export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction {
return {
type: TypeKeys.SWAP_ORDER_TIME,
payload

View File

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

View File

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

View File

@ -1,23 +1,31 @@
import bityConfig from 'config/bity';
import { checkHttpStatus, parseJSON } from './utils';
import bityConfig, { WhitelistedCoins } from 'config/bity';
import { checkHttpStatus, parseJSON, filter } from './utils';
const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => {
return filter(from, arr) && filter(to, arr);
};
export function getAllRates() {
const mappedRates = {};
return _getAllRates().then(bityRates => {
bityRates.objects.forEach(each => {
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;
});
}
export function postOrder(
amount: number,
destAddress: string,
mode: number,
pair: string
) {
export function postOrder(amount: number, destAddress: string, mode: number, pair: string) {
return fetch(`${bityConfig.serverURL}/order`, {
method: 'post',
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) {
if (response.status >= 200 && response.status < 300) {
return response;

View File

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

View File

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

View File

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

View File

@ -1,267 +1,218 @@
import { TShowNotification } from 'actions/notifications';
import {
TChangeStepSwap,
TDestinationAmountSwap,
TDestinationKindSwap,
TOriginAmountSwap,
TOriginKindSwap
} from 'actions/swap';
import { TChangeStepSwap, TInitSwap } from 'actions/swap';
import { NormalizedBityRates, NormalizedOptions, SwapInput } from 'reducers/swap/types';
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 translate from 'translations';
import { combineAndUpper, toFixedIfLarger } from 'utils/formatters';
import './CurrencySwap.scss';
import { combineAndUpper } from 'utils/formatters';
import { Dropdown } from 'components/ui';
import Spinner from 'components/ui/Spinner';
import { without, intersection } from 'lodash';
import './CurrencySwap.scss';
export interface StateProps {
bityRates: any;
originAmount: number | null;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
bityRates: NormalizedBityRates;
options: NormalizedOptions;
}
export interface ActionProps {
showNotification: TShowNotification;
changeStepSwap: TChangeStepSwap;
originKindSwap: TOriginKindSwap;
destinationKindSwap: TDestinationKindSwap;
originAmountSwap: TOriginAmountSwap;
destinationAmountSwap: TDestinationAmountSwap;
initSwap: TInitSwap;
}
interface State {
disabled: boolean;
showedMinMaxError: boolean;
origin: SwapInput;
destination: SwapInput;
originKindOptions: WhitelistedCoins[];
destinationKindOptions: WhitelistedCoins[];
originErr: string;
destinationErr: string;
}
export default class CurrencySwap extends Component<
StateProps & ActionProps,
State
> {
type Props = StateProps & ActionProps;
export default class CurrencySwap extends Component<Props, State> {
public state = {
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: '',
destinationErr: ''
};
public componentWillReceiveProps(newProps) {
const {
originAmount,
originKind,
destinationKind,
destinationAmount
} = newProps;
if (
originKind !== this.props.originKind ||
destinationKind !== this.props.destinationKind
) {
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmount
public componentDidUpdate(prevProps: Props, prevState: State) {
const { origin, destination } = 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
);
const destinationKindOptions: WhitelistedCoins[] = without<any>(options.allIds, origin.id);
this.setState({
originKindOptions,
destinationKindOptions
});
}
}
public isMinMaxValid = (amount, kind) => {
let bityMin;
let bityMax;
public getMinMax = (kind: WhitelistedCoins) => {
let min;
let max;
if (kind !== 'BTC') {
const bityPairRate = this.props.bityRates['BTC' + kind];
bityMin = generateKindMin(bityPairRate, kind);
bityMax = generateKindMax(bityPairRate, kind);
const bityPairRate = this.props.bityRates.byId['BTC' + kind].rate;
min = generateKindMin(bityPairRate, kind);
max = generateKindMax(bityPairRate, kind);
} else {
bityMin = bityConfig.BTCMin;
bityMax = bityConfig.BTCMax;
min = bityConfig.BTCMin;
max = bityConfig.BTCMax;
}
const higherThanMin = amount >= bityMin;
const lowerThanMax = amount <= bityMax;
return { min, max };
};
public isMinMaxValid = (amount: number, kind: WhitelistedCoins) => {
const rate = this.getMinMax(kind);
const higherThanMin = amount >= rate.min;
const lowerThanMax = amount <= rate.max;
return higherThanMin && lowerThanMax;
};
public isDisabled = (originAmount, originKind, destinationAmount) => {
const hasOriginAmountAndDestinationAmount =
originAmount && destinationAmount;
const minMaxIsValid = this.isMinMaxValid(originAmount, originKind);
return !(hasOriginAmountAndDestinationAmount && minMaxIsValid);
};
public setDisabled(origin: SwapInput, destination: SwapInput) {
const amountsValid = origin.amount && destination.amount;
const minMaxValid = this.isMinMaxValid(origin.amount, origin.id);
public setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmount
) {
const disabled = this.isDisabled(
originAmount,
originKind,
destinationAmount
);
const disabled = !(amountsValid && minMaxValid);
if (disabled && originAmount) {
const { bityRates } = this.props;
const ETHMin = generateKindMin(bityRates.BTCETH, 'ETH');
const ETHMax = generateKindMax(bityRates.BTCETH, 'ETH');
const REPMin = generateKindMin(bityRates.BTCREP, 'REP');
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 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 showError = disabled && amountsValid;
const originErr = showError ? createErrString(origin.id, origin.amount) : '';
const destinationErr = showError ? createErrString(destination.id, destination.amount) : '';
const createErrString = (kind, amount, rate) => {
let errString;
if (amount > rate.maxAmount) {
errString = `Maximum ${kind} is ${rate.maxAmount} ${kind}`;
} else {
errString = `Minimum ${kind} is ${rate.minAmount} ${kind}`;
}
return errString;
};
const originRate = getRates(originKind);
const destinationRate = getRates(destinationKind);
const originErr = createErrString(originKind, originAmount, originRate);
const destinationErr = createErrString(
destinationKind,
destinationAmount,
destinationRate
);
this.setState({
originErr,
destinationErr,
disabled: true
});
} else {
this.setState({
originErr: '',
destinationErr: '',
disabled
});
}
this.setState({
disabled,
originErr,
destinationErr
});
}
public onClickStartSwap = () => {
this.props.changeStepSwap(2);
const { origin, destination } = this.state;
const { changeStepSwap, initSwap } = this.props;
initSwap({ origin, destination });
changeStepSwap(2);
};
public setOriginAndDestinationToNull = () => {
this.props.originAmountSwap(null);
this.props.destinationAmountSwap(null);
this.setDisabled(
null,
this.props.originKind,
this.props.destinationKind,
null
);
public setOriginAndDestinationToInitialVal = () => {
this.setState({
origin: { ...this.state.origin, amount: NaN },
destination: { ...this.state.destination, amount: NaN }
});
};
public onChangeOriginAmount = (
event: React.SyntheticEvent<HTMLInputElement>
) => {
const { destinationKind, originKind } = this.props;
const amount = (event.target as HTMLInputElement).value;
const originAmountAsNumber = parseFloat(amount);
if (originAmountAsNumber || originAmountAsNumber === 0) {
const pairName = combineAndUpper(originKind, destinationKind);
const bityRate = this.props.bityRates[pairName];
this.props.originAmountSwap(originAmountAsNumber);
const destinationAmount = originAmountAsNumber * bityRate;
this.props.destinationAmountSwap(destinationAmount);
this.setDisabled(
originAmountAsNumber,
originKind,
destinationKind,
destinationAmount
);
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;
this.setState({
origin: { ...this.state.origin, amount },
destination: { ...this.state.destination, amount: destinationAmount }
});
} else {
this.setOriginAndDestinationToNull();
this.setOriginAndDestinationToInitialVal();
}
};
public onChangeDestinationAmount = (
event: React.SyntheticEvent<HTMLInputElement>
) => {
const { destinationKind, originKind } = this.props;
const amount = (event.target as HTMLInputElement).value;
const destinationAmountAsNumber = parseFloat(amount);
if (destinationAmountAsNumber || destinationAmountAsNumber === 0) {
this.props.destinationAmountSwap(destinationAmountAsNumber);
const pairNameReversed = combineAndUpper(destinationKind, originKind);
const bityRate = this.props.bityRates[pairNameReversed];
const originAmount = destinationAmountAsNumber * bityRate;
this.props.originAmountSwap(originAmount);
this.setDisabled(
originAmount,
originKind,
destinationKind,
destinationAmountAsNumber
);
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;
this.setState({
origin: { ...this.state.origin, amount: originAmount },
destination: {
...this.state.destination,
amount
}
});
} else {
this.setOriginAndDestinationToNull();
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() {
const { bityRates } = this.props;
const {
originAmount,
destinationAmount,
originKind,
destinationKind,
destinationKindOptions,
origin,
destination,
originKindOptions,
bityRates
} = this.props;
destinationKindOptions,
originErr,
destinationErr
} = this.state;
const { originErr, destinationErr } = this.state;
const OriginKindDropDown = Dropdown as new () => Dropdown<
typeof originKind
>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<
typeof destinationKind
>;
const pairName = combineAndUpper(originKind, destinationKind);
const bityLoaded = bityRates[pairName];
const OriginKindDropDown = Dropdown as new () => Dropdown<any>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<typeof destination.id>;
const pairName = combineAndUpper(origin.id, destination.id);
const bityLoaded = bityRates.byId[pairName] ? bityRates.byId[pairName].id : false;
return (
<article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
@ -270,25 +221,23 @@ export default class CurrencySwap extends Component<
<div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message">{originErr}</span>
<input
id="origin-swap-input"
className={`CurrencySwap-input form-control ${
String(originAmount) !== '' &&
this.isMinMaxValid(originAmount, originKind)
String(origin.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={originAmount || originAmount === 0 ? originAmount : ''}
onChange={this.onChangeOriginAmount}
value={isNaN(origin.amount) ? '' : origin.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<OriginKindDropDown
ariaLabel={`change origin kind. current origin kind ${
originKind
}`}
ariaLabel={`change origin kind. current origin kind ${origin.id}`}
options={originKindOptions}
value={originKind}
onChange={this.props.originKindSwap}
value={origin.id}
onChange={this.onChangeOriginKind}
size="smr"
color="default"
/>
@ -296,33 +245,25 @@ export default class CurrencySwap extends Component<
</div>
<h1 className="CurrencySwap-divider">{translate('SWAP_init_2')}</h1>
<div className="CurrencySwap-input-group">
<span className="CurrencySwap-error-message">
{destinationErr}
</span>
<span className="CurrencySwap-error-message">{destinationErr}</span>
<input
id="destination-swap-input"
className={`CurrencySwap-input form-control ${
String(destinationAmount) !== '' &&
this.isMinMaxValid(originAmount, originKind)
String(destination.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={
destinationAmount || destinationAmount === 0
? destinationAmount
: ''
}
onChange={this.onChangeDestinationAmount}
value={isNaN(destination.amount) ? '' : destination.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
<DestinationKindDropDown
ariaLabel={`change destination kind. current destination kind ${
destinationKind
}`}
ariaLabel={`change destination kind. current destination kind ${destination.id}`}
options={destinationKindOptions}
value={destinationKind}
onChange={this.props.destinationKindSwap}
value={destination.id}
onChange={this.onChangeDestinationKind}
size="smr"
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 Spinner from 'components/ui/Spinner';
import { bityReferralURL } from 'config/data';
@ -7,13 +7,18 @@ import translate from 'translations';
import { toFixedIfLarger } from 'utils/formatters';
import './CurrentRates.scss';
interface Props {
[id: string]: NormalizedBityRate;
}
interface State {
ETHBTCAmount: number;
ETHREPAmount: number;
BTCETHAmount: number;
BTCREPAmount: number;
}
export default class CurrentRates extends Component<Pairs, State> {
export default class CurrentRates extends Component<Props, State> {
public state = {
ETHBTCAmount: 1,
ETHREPAmount: 1,
@ -32,7 +37,7 @@ export default class CurrentRates extends Component<Pairs, State> {
public buildPairRate = (origin: string, destination: string) => {
const pair = origin + destination;
const statePair = this.state[pair + 'Amount'];
const propsPair = this.props[pair];
const propsPair = this.props[pair] ? this.props[pair].rate : null;
return (
<div className="SwapRates-panel-rate">
{propsPair ? (
@ -44,9 +49,7 @@ export default class CurrentRates extends Component<Pairs, State> {
name={pair + 'Amount'}
/>
<span className="SwapRates-panel-rate-amount">
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${
destination
}`}
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${destination}`}
</span>
</div>
) : (
@ -71,11 +74,7 @@ export default class CurrentRates extends Component<Pairs, State> {
{this.buildPairRate('BTC', 'ETH')}
{this.buildPairRate('BTC', 'REP')}
</div>
<a
className="SwapRates-panel-logo"
href={bityReferralURL}
target="_blank"
>
<a className="SwapRates-panel-logo" href={bityReferralURL} target="_blank">
<img src={bityLogoWhite} width={120} height={49} />
</a>
</section>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,21 @@
import { showNotification as dShowNotification, TShowNotification } from 'actions/notifications';
import {
showNotification as dShowNotification,
TShowNotification
} from 'actions/notifications';
import {
initSwap as dInitSwap,
bityOrderCreateRequestedSwap as dBityOrderCreateRequestedSwap,
changeStepSwap as dChangeStepSwap,
destinationAddressSwap as dDestinationAddressSwap,
destinationAmountSwap as dDestinationAmountSwap,
destinationKindSwap as dDestinationKindSwap,
loadBityRatesRequestedSwap as dLoadBityRatesRequestedSwap,
originAmountSwap as dOriginAmountSwap,
originKindSwap as dOriginKindSwap,
restartSwap as dRestartSwap,
startOrderTimerSwap as dStartOrderTimerSwap,
startPollBityOrderStatus as dStartPollBityOrderStatus,
stopLoadBityRatesSwap as dStopLoadBityRatesSwap,
stopOrderTimerSwap as dStopOrderTimerSwap,
stopPollBityOrderStatus as dStopPollBityOrderStatus,
TInitSwap,
TBityOrderCreateRequestedSwap,
TChangeStepSwap,
TDestinationAddressSwap,
TDestinationAmountSwap,
TDestinationKindSwap,
TLoadBityRatesRequestedSwap,
TOriginAmountSwap,
TOriginKindSwap,
TRestartSwap,
TStartOrderTimerSwap,
TStartPollBityOrderStatus,
@ -32,6 +23,7 @@ import {
TStopOrderTimerSwap,
TStopPollBityOrderStatus
} from 'actions/swap';
import { SwapInput, NormalizedOptions, NormalizedBityRates } from 'reducers/swap/types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
@ -43,14 +35,11 @@ import SwapInfoHeader from './components/SwapInfoHeader';
import TabSection from 'containers/TabSection';
interface ReduxStateProps {
originAmount: number | null;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
step: number;
bityRates: any;
origin: SwapInput;
destination: SwapInput;
bityRates: NormalizedBityRates;
options: NormalizedOptions;
bityOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
@ -63,10 +52,6 @@ interface ReduxStateProps {
interface ReduxActionProps {
changeStepSwap: TChangeStepSwap;
originKindSwap: TOriginKindSwap;
destinationKindSwap: TDestinationKindSwap;
originAmountSwap: TOriginAmountSwap;
destinationAmountSwap: TDestinationAmountSwap;
loadBityRatesRequestedSwap: TLoadBityRatesRequestedSwap;
destinationAddressSwap: TDestinationAddressSwap;
restartSwap: TRestartSwap;
@ -77,11 +62,11 @@ interface ReduxActionProps {
stopPollBityOrderStatus: TStopPollBityOrderStatus;
showNotification: TShowNotification;
startOrderTimerSwap: TStartOrderTimerSwap;
initSwap: TInitSwap;
}
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() {
// TODO: Use `isFetchingRates` to show a loader
this.props.loadBityRatesRequestedSwap();
}
@ -93,12 +78,9 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
const {
// STATE
bityRates,
originAmount,
destinationAmount,
originKind,
destinationKind,
destinationKindOptions,
originKindOptions,
options,
origin,
destination,
destinationAddress,
step,
bityOrder,
@ -108,13 +90,10 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
isPostingOrder,
outputTx,
// ACTIONS
initSwap,
restartSwap,
stopLoadBityRatesSwap,
changeStepSwap,
originKindSwap,
destinationKindSwap,
originAmountSwap,
destinationAmountSwap,
destinationAddressSwap,
bityOrderCreateRequestedSwap,
showNotification,
@ -128,9 +107,8 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
const ReceivingAddressProps = {
isPostingOrder,
originAmount,
originKind,
destinationKind,
origin,
destinationId: destination.id,
destinationAddressSwap,
destinationAddress,
stopLoadBityRatesSwap,
@ -139,38 +117,24 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
};
const SwapInfoHeaderProps = {
origin,
destination,
reference,
secondsRemaining,
originAmount,
originKind,
destinationKind,
destinationAmount,
restartSwap,
orderStatus
};
const { ETHBTC, ETHREP, BTCETH, BTCREP } = bityRates;
const CurrentRatesProps = { ETHBTC, ETHREP, BTCETH, BTCREP };
const CurrencySwapProps = {
showNotification,
bityRates,
originAmount,
destinationAmount,
originKind,
destinationKind,
destinationKindOptions,
originKindOptions,
originKindSwap,
destinationKindSwap,
originAmountSwap,
destinationAmountSwap,
options,
initSwap,
changeStepSwap
};
const PaymentInfoProps = {
originKind,
originAmount,
origin,
paymentAddress
};
@ -187,13 +151,14 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
outputTx
};
const { ETHBTC, ETHREP, BTCETH, BTCREP } = bityRates.byId;
const CurrentRatesProps = { ETHBTC, ETHREP, BTCETH, BTCREP };
return (
<TabSection>
<section className="Tab-content swap-tab">
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
{(step === 2 || step === 3) && (
<SwapInfoHeader {...SwapInfoHeaderProps} />
)}
{(step === 2 || step === 3) && <SwapInfoHeader {...SwapInfoHeaderProps} />}
<main className="Tab-content-pane">
{step === 1 && <CurrencySwap {...CurrencySwapProps} />}
@ -208,14 +173,11 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
function mapStateToProps(state: AppState) {
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,
origin: state.swap.origin,
destination: state.swap.destination,
bityRates: state.swap.bityRates,
options: state.swap.options,
bityOrder: state.swap.bityOrder,
destinationAddress: state.swap.destinationAddress,
isFetchingRates: state.swap.isFetchingRates,
@ -228,14 +190,11 @@ function mapStateToProps(state: AppState) {
}
export default connect(mapStateToProps, {
bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
changeStepSwap: dChangeStepSwap,
destinationAddressSwap: dDestinationAddressSwap,
destinationAmountSwap: dDestinationAmountSwap,
destinationKindSwap: dDestinationKindSwap,
initSwap: dInitSwap,
bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
loadBityRatesRequestedSwap: dLoadBityRatesRequestedSwap,
originAmountSwap: dOriginAmountSwap,
originKindSwap: dOriginKindSwap,
destinationAddressSwap: dDestinationAddressSwap,
restartSwap: dRestartSwap,
startOrderTimerSwap: dStartOrderTimerSwap,
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 stateTypes from './types';
import * as schema from './schema';
import { TypeKeys } from 'actions/swap/constants';
import without from 'lodash/without';
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';
import { normalize } from 'normalizr';
export interface State {
originAmount: number | null;
destinationAmount: number | null;
originKind: string;
destinationKind: string;
destinationKindOptions: string[];
originKindOptions: string[];
step: number;
bityRates: any;
origin: stateTypes.SwapInput;
destination: stateTypes.SwapInput;
options: stateTypes.NormalizedOptions;
bityRates: stateTypes.NormalizedBityRates;
bityOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
@ -33,14 +24,17 @@ export interface 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,
bityRates: {},
origin: { id: 'BTC', amount: NaN },
destination: { id: 'ETH', amount: NaN },
options: {
byId: {},
allIds: []
},
bityRates: {
byId: {},
allIds: []
},
destinationAddress: '',
bityOrder: {},
isFetchingRates: null,
@ -54,76 +48,29 @@ export const INITIAL_STATE: State = {
orderId: null
};
function handleSwapOriginKind(
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
) {
export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapAction) {
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:
const { payload } = action;
return {
...state,
bityRates: {
...state.bityRates,
...action.payload
byId: normalize(payload, [schema.bityRate]).entities.bityRates,
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
};
case TypeKeys.SWAP_INIT: {
return {
...state,
origin: action.payload.origin,
destination: action.payload.destination
};
}
case TypeKeys.SWAP_STEP: {
return {
...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 { routerMiddleware } from 'react-router-redux';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { INITIAL_STATE as customTokensInitialState } from 'reducers/customTokens';
import { INITIAL_STATE as swapInitialState } from 'reducers/swap';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
import {
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 { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
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 { getNodeConfigFromId } from 'utils/node';
@ -54,13 +54,9 @@ const configureStore = () => {
}
: { ...swapInitialState };
const localCustomTokens = loadStatePropertyOrEmptyObject<CustomTokenState>(
'customTokens'
);
const localCustomTokens = loadStatePropertyOrEmptyObject<CustomTokenState>('customTokens');
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>(
'config'
);
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
// 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.
@ -90,8 +86,7 @@ const configureStore = () => {
// if 'web3' has persisted as node selection, reset to app default
// necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
if (persistedInitialState.config.nodeSelection === 'web3') {
persistedInitialState.config.nodeSelection =
configInitialState.nodeSelection;
persistedInitialState.config.nodeSelection = configInitialState.nodeSelection;
}
store = createStore(RootReducer, persistedInitialState, middleware);
@ -112,7 +107,11 @@ const configureStore = () => {
customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks
},
swap: { ...state.swap, bityRates: {} },
swap: {
...state.swap,
options: {},
bityRates: {}
},
customTokens: state.customTokens
});
}),

View File

@ -23,6 +23,7 @@
"jsonschema": "1.2.0",
"lodash": "4.17.4",
"moment": "2.19.3",
"normalizr": "3.2.4",
"qrcode": "1.0.0",
"qrcode.react": "0.7.2",
"query-string": "5.0.1",
@ -113,7 +114,7 @@
},
"scripts": {
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
"freezer:validate": "npm run freezer -- --validate",
"freezer:validate": "npm run freezer -- --validate",
"db": "nodemon ./db",
"build": "webpack --config webpack_config/webpack.prod.js",
"prebuild": "check-node-version --package",

View File

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

View File

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

View File

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

View File

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