aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/instant/src/components/buy_button.tsx11
-rw-r--r--packages/instant/src/components/buy_order_state_buttons.tsx3
-rw-r--r--packages/instant/src/components/simulated_progress_bar.tsx194
-rw-r--r--packages/instant/src/components/zero_ex_instant.tsx2
-rw-r--r--packages/instant/src/components/zero_ex_instant_container.tsx4
-rw-r--r--packages/instant/src/constants.ts3
-rw-r--r--packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts18
-rw-r--r--packages/instant/src/containers/selected_asset_simulated_progress_bar.tsx43
-rw-r--r--packages/instant/src/containers/selected_erc20_asset_amount_input.ts2
-rw-r--r--packages/instant/src/redux/actions.ts15
-rw-r--r--packages/instant/src/redux/reducer.ts66
-rw-r--r--packages/instant/src/types.ts7
-rw-r--r--packages/instant/src/util/gas_price_estimator.ts32
-rw-r--r--packages/instant/src/util/time.ts39
-rw-r--r--packages/instant/test/util/time.test.ts48
-rw-r--r--packages/instant/tslint.json3
16 files changed, 455 insertions, 35 deletions
diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx
index 93bd8e635..c00b1678d 100644
--- a/packages/instant/src/components/buy_button.tsx
+++ b/packages/instant/src/components/buy_button.tsx
@@ -19,7 +19,7 @@ export interface BuyButtonProps {
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
onSignatureDenied: (buyQuote: BuyQuote) => void;
- onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void;
+ onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => void;
onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void;
onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void;
}
@@ -57,9 +57,9 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
let txHash: string | undefined;
- const gasPrice = await gasPriceEstimator.getFastAmountInWeiAsync();
+ const gasInfo = await gasPriceEstimator.getGasInfoAsync();
try {
- txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { takerAddress, gasPrice });
+ txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { takerAddress, gasPrice: gasInfo.gasPriceInWei });
} catch (e) {
if (e instanceof Error) {
if (e.message === AssetBuyerError.SignatureRequestDenied) {
@@ -73,7 +73,9 @@ export class BuyButton extends React.Component<BuyButtonProps> {
throw e;
}
- this.props.onBuyProcessing(buyQuote, txHash);
+ const startTimeUnix = new Date().getTime();
+ const expectedEndTimeUnix = startTimeUnix + gasInfo.estimatedTimeMs;
+ this.props.onBuyProcessing(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
try {
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
} catch (e) {
@@ -83,6 +85,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
throw e;
}
+
this.props.onBuySuccess(buyQuote, txHash);
};
}
diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx
index d01e9ff57..3f0764062 100644
--- a/packages/instant/src/components/buy_order_state_buttons.tsx
+++ b/packages/instant/src/components/buy_order_state_buttons.tsx
@@ -20,13 +20,12 @@ export interface BuyOrderStateButtonProps {
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
onSignatureDenied: (buyQuote: BuyQuote) => void;
- onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void;
+ onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => void;
onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void;
onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void;
onRetry: () => void;
}
-// TODO: rename to buttons
export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonProps> = props => {
if (props.buyOrderProcessingState === OrderProcessState.FAILURE) {
return (
diff --git a/packages/instant/src/components/simulated_progress_bar.tsx b/packages/instant/src/components/simulated_progress_bar.tsx
new file mode 100644
index 000000000..067f4093e
--- /dev/null
+++ b/packages/instant/src/components/simulated_progress_bar.tsx
@@ -0,0 +1,194 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+
+import { PROGRESS_STALL_AT_PERCENTAGE, PROGRESS_TICK_INTERVAL_MS } from '../constants';
+import { ColorOption, styled } from '../style/theme';
+import { timeUtil } from '../util/time';
+
+import { Container } from './ui/container';
+import { Flex } from './ui/flex';
+import { Text } from './ui/text';
+
+const TICKS_PER_SECOND = 1000 / PROGRESS_TICK_INTERVAL_MS;
+
+const curTimeUnix = () => {
+ return new Date().getTime();
+};
+
+export interface SimulatedProgressBarProps {
+ startTimeUnix: number;
+ expectedEndTimeUnix: number;
+ ended: boolean;
+}
+enum TickingRunState {
+ None = 'None',
+ Running = 'Running',
+ Finishing = 'Finishing',
+}
+interface TickingNoneStatus {
+ runState: TickingRunState.None;
+}
+interface TickingRunningStatus {
+ runState: TickingRunState.Running;
+}
+interface TickingFinishingStatus {
+ runState: TickingRunState.Finishing;
+ increasePercentageEveryTick: number;
+}
+type TickingStatus = TickingNoneStatus | TickingRunningStatus | TickingFinishingStatus;
+
+export interface SimulatedProgressState {
+ percentageDone: number;
+ intervalId?: number;
+ tickingStatus: TickingStatus;
+ elapsedTimeMs: number;
+}
+export class SimulatedProgressBar extends React.Component<SimulatedProgressBarProps, SimulatedProgressState> {
+ public constructor(props: SimulatedProgressBarProps) {
+ super(props);
+
+ // TODO: look into using assert library here?
+ if (props.expectedEndTimeUnix <= props.startTimeUnix) {
+ throw new Error('End time before start time');
+ }
+
+ // TODO: use getFreshState here
+ const intervalId = window.setInterval(this._tick.bind(this), PROGRESS_TICK_INTERVAL_MS);
+ this.state = {
+ percentageDone: 0,
+ intervalId,
+ tickingStatus: { runState: TickingRunState.Running },
+ elapsedTimeMs: 0,
+ };
+ }
+
+ public componentDidUpdate(prevProps: SimulatedProgressBarProps, prevState: SimulatedProgressState): void {
+ const percentLeft = 100 - this.state.percentageDone;
+ const increasePercentageEveryTick = percentLeft / TICKS_PER_SECOND;
+
+ // if we just switched to ending, having animate to end
+ if (prevProps.ended === false && this.props.ended === true) {
+ this.setState({
+ tickingStatus: {
+ runState: TickingRunState.Finishing,
+ increasePercentageEveryTick,
+ },
+ });
+ return;
+ }
+
+ // later TODO: the new state could be for the wrong order, attach to order state or add concurrency checking
+
+ // if anything else changes, reset internal state
+ if (
+ prevProps.startTimeUnix !== this.props.startTimeUnix ||
+ prevProps.expectedEndTimeUnix !== this.props.expectedEndTimeUnix ||
+ prevProps.ended !== this.props.ended
+ ) {
+ this.setState(this._getFreshState());
+ }
+ }
+
+ public componentWillUnmount(): void {
+ console.log('unmount');
+ this._clearTimer();
+ }
+
+ public render(): React.ReactNode {
+ // TODO: Consider moving to seperate component
+
+ const estimatedTimeSeconds = Math.ceil((this.props.expectedEndTimeUnix - this.props.startTimeUnix) / 1000);
+ const elapsedTimeSeconds = Math.floor(this.state.elapsedTimeMs / 1000);
+ return (
+ <Container padding="20px 20px 0px 20px" width="100%">
+ <Container marginBottom="5px">
+ {/* TODO: consider moving to separate component */}
+ <Flex justify="space-between">
+ <Text>Est. Time ({timeUtil.secondsToHumanDescription(estimatedTimeSeconds)})</Text>
+ <Text>Time: {timeUtil.secondsToStopwatchTime(elapsedTimeSeconds)}</Text>
+ </Flex>
+ </Container>
+ <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px">
+ <InnerProgressBarElement
+ percentageDone={this.state.percentageDone}
+ backgroundColor={ColorOption.primaryColor}
+ borderRadius="6px"
+ height="6px"
+ transitionTimeMs={200}
+ />
+ </Container>
+ </Container>
+ );
+ }
+
+ private _getFreshState(): SimulatedProgressState {
+ this._clearTimer();
+ const intervalId = window.setInterval(this._tick.bind(this), PROGRESS_TICK_INTERVAL_MS);
+ return {
+ percentageDone: 0,
+ intervalId,
+ tickingStatus: { runState: TickingRunState.Running },
+ elapsedTimeMs: 0,
+ };
+ }
+
+ private _tick(): void {
+ const rawPercentageDone =
+ this.state.tickingStatus.runState === TickingRunState.Finishing
+ ? this._getNewPercentageFinishing(this.state.tickingStatus)
+ : this._getNewPercentageNormal();
+ const maxPercentage =
+ this.state.tickingStatus.runState === TickingRunState.Finishing ? 100 : PROGRESS_STALL_AT_PERCENTAGE;
+ const percentageDone = Math.min(rawPercentageDone, maxPercentage);
+
+ const elapsedTimeMs = Math.max(curTimeUnix() - this.props.startTimeUnix, 0);
+
+ this.setState({
+ percentageDone,
+ elapsedTimeMs,
+ });
+
+ if (percentageDone >= 100) {
+ this._clearTimer();
+ }
+ }
+
+ private _clearTimer(): void {
+ if (this.state.intervalId) {
+ window.clearTimeout(this.state.intervalId);
+ }
+ }
+
+ // TODO: consider not taking in a parameter here, might be confusing
+ private _getNewPercentageFinishing(tickingStatus: TickingFinishingStatus): number {
+ return this.state.percentageDone + tickingStatus.increasePercentageEveryTick;
+ }
+
+ private _getNewPercentageNormal(): number {
+ const elapsedTimeMs = curTimeUnix() - this.props.startTimeUnix;
+ const safeElapsedTimeMs = Math.max(elapsedTimeMs, 1);
+
+ const expectedAmountOfTimeMs = this.props.expectedEndTimeUnix - this.props.startTimeUnix;
+ const percentageDone = safeElapsedTimeMs / expectedAmountOfTimeMs * 100;
+ return percentageDone;
+ }
+}
+
+interface InnerProgressBarElementProps {
+ percentageDone: number;
+ backgroundColor: ColorOption;
+ borderRadius: string;
+ height: string;
+ transitionTimeMs: number;
+}
+
+export const InnerProgressBarElement =
+ styled.div <
+ InnerProgressBarElementProps >
+ `
+ width: ${props => props.percentageDone}%;
+ background-color: ${props => props.theme[props.backgroundColor]};
+ border-radius: ${props => props.borderRadius};
+ height: ${props => props.height};
+ transition: width ${props => props.transitionTimeMs}ms ease-in-out;
+ `;
diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx
index d54dfc153..365c1610f 100644
--- a/packages/instant/src/components/zero_ex_instant.tsx
+++ b/packages/instant/src/components/zero_ex_instant.tsx
@@ -83,7 +83,7 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> {
// warm up the gas price estimator cache just in case we can't
// grab the gas price estimate when submitting the transaction
// tslint:disable-next-line:no-floating-promises
- gasPriceEstimator.getFastAmountInWeiAsync();
+ gasPriceEstimator.getGasInfoAsync();
// tslint:disable-next-line:no-floating-promises
this._flashErrorIfWrongNetwork();
diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx
index ff19351ff..6b1042668 100644
--- a/packages/instant/src/components/zero_ex_instant_container.tsx
+++ b/packages/instant/src/components/zero_ex_instant_container.tsx
@@ -5,10 +5,11 @@ import { LatestError } from '../containers/latest_error';
import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons';
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
+import { SelectedAssetSimulatedProgressBar } from '../containers/selected_asset_simulated_progress_bar';
+
import { ColorOption } from '../style/theme';
import { Container, Flex } from './ui';
-
export interface ZeroExInstantContainerProps {}
export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantContainerProps> = props => (
@@ -25,6 +26,7 @@ export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantConta
>
<Flex direction="column" justify="flex-start">
<SelectedAssetInstantHeading />
+ <SelectedAssetSimulatedProgressBar />
<LatestBuyQuoteOrderDetails />
<Container padding="20px" width="100%">
<SelectedAssetBuyOrderStateButtons />
diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts
index 424f35ecb..3b320ed36 100644
--- a/packages/instant/src/constants.ts
+++ b/packages/instant/src/constants.ts
@@ -5,5 +5,8 @@ export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer';
export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed';
export const GWEI_IN_WEI = new BigNumber(1000000000);
export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6);
+export const DEFAULT_ESTIMATED_TRANSACTION_TIME_MS = 2 * 60 * 1000; // 2 minutes
export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2';
+export const PROGRESS_TICK_INTERVAL_MS = 250;
+export const PROGRESS_STALL_AT_PERCENTAGE = 95;
diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
index 500d6b88a..a94538ffc 100644
--- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
+++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
@@ -21,7 +21,7 @@ interface ConnectedState {
interface ConnectedDispatch {
onValidationPending: (buyQuote: BuyQuote) => void;
onSignatureDenied: (buyQuote: BuyQuote) => void;
- onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void;
+ onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => void;
onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void;
onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void;
onRetry: () => void;
@@ -56,24 +56,20 @@ const mapDispatchToProps = (
ownProps: SelectedAssetBuyOrderStateButtons,
): ConnectedDispatch => ({
onValidationPending: (buyQuote: BuyQuote) => {
- const newOrderState: OrderState = { processState: OrderProcessState.VALIDATING };
- dispatch(actions.updateBuyOrderState(newOrderState));
+ dispatch(actions.setBuyOrderStateValidating());
},
- onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => {
- const newOrderState: OrderState = { processState: OrderProcessState.PROCESSING, txHash };
- dispatch(actions.updateBuyOrderState(newOrderState));
+ onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => {
+ dispatch(actions.setBuyOrderStateProcessing(txHash, startTimeUnix, expectedEndTimeUnix));
},
- onBuySuccess: (buyQuote: BuyQuote, txHash: string) =>
- dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.SUCCESS, txHash })),
- onBuyFailure: (buyQuote: BuyQuote, txHash: string) =>
- dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.FAILURE, txHash })),
+ onBuySuccess: (buyQuote: BuyQuote, txHash: string) => dispatch(actions.setBuyOrderStateSuccess(txHash)),
+ onBuyFailure: (buyQuote: BuyQuote, txHash: string) => dispatch(actions.setBuyOrderStateFailure(txHash)),
onSignatureDenied: () => {
dispatch(actions.resetAmount());
const errorMessage = 'You denied this transaction';
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
},
onValidationFail: (buyQuote, error) => {
- dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.NONE }));
+ dispatch(actions.setBuyOrderStateNone());
if (error === ZeroExInstantError.InsufficientETH) {
const errorMessage = "You don't have enough ETH";
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
diff --git a/packages/instant/src/containers/selected_asset_simulated_progress_bar.tsx b/packages/instant/src/containers/selected_asset_simulated_progress_bar.tsx
new file mode 100644
index 000000000..a7acc4cb7
--- /dev/null
+++ b/packages/instant/src/containers/selected_asset_simulated_progress_bar.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+
+import { connect } from 'react-redux';
+
+import { SimulatedProgressBar } from '../components/simulated_progress_bar';
+
+import { State } from '../redux/reducer';
+import { OrderProcessState, OrderState, SimulatedProgress } from '../types';
+
+interface SelectedAssetProgressComponentProps {
+ buyOrderState: OrderState;
+}
+export const SelectedAssetSimulatedProgressComponent: React.StatelessComponent<
+ SelectedAssetProgressComponentProps
+> = props => {
+ const { buyOrderState } = props;
+
+ if (
+ buyOrderState.processState === OrderProcessState.PROCESSING ||
+ buyOrderState.processState === OrderProcessState.SUCCESS ||
+ buyOrderState.processState === OrderProcessState.FAILURE
+ ) {
+ const progress = buyOrderState.progress;
+ return (
+ <SimulatedProgressBar
+ startTimeUnix={progress.startTimeUnix}
+ expectedEndTimeUnix={progress.expectedEndTimeUnix}
+ ended={progress.ended}
+ />
+ );
+ }
+
+ return null;
+};
+
+interface ConnectedState {
+ buyOrderState: OrderState;
+ simulatedProgress?: SimulatedProgress;
+}
+const mapStateToProps = (state: State, _ownProps: {}): ConnectedState => ({
+ buyOrderState: state.buyOrderState,
+});
+export const SelectedAssetSimulatedProgressBar = connect(mapStateToProps)(SelectedAssetSimulatedProgressComponent);
diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
index 4767b15d4..c0245f721 100644
--- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
+++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
@@ -113,7 +113,7 @@ const mapDispatchToProps = (
// invalidate the last buy quote.
dispatch(actions.updateLatestBuyQuote(undefined));
// reset our buy state
- dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.NONE }));
+ dispatch(actions.setBuyOrderStateNone());
if (!_.isUndefined(value) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) {
// even if it's debounced, give them the illusion it's loading
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
index bfae68e2b..627e39ffc 100644
--- a/packages/instant/src/redux/actions.ts
+++ b/packages/instant/src/redux/actions.ts
@@ -4,7 +4,7 @@ import * as _ from 'lodash';
import { BigNumberInput } from '../util/big_number_input';
-import { ActionsUnion, OrderState } from '../types';
+import { ActionsUnion, OrderState, SimulatedProgress } from '../types';
export interface PlainAction<T extends string> {
type: T;
@@ -25,7 +25,11 @@ function createAction<T extends string, P>(type: T, data?: P): PlainAction<T> |
export enum ActionTypes {
UPDATE_ETH_USD_PRICE = 'UPDATE_ETH_USD_PRICE',
UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT',
- UPDATE_BUY_ORDER_STATE = 'UPDATE_BUY_ORDER_STATE',
+ SET_BUY_ORDER_STATE_NONE = 'SET_BUY_ORDER_STATE_NONE',
+ SET_BUY_ORDER_STATE_VALIDATING = 'SET_BUY_ORDER_STATE_VALIDATING',
+ SET_BUY_ORDER_STATE_PROCESSING = 'SET_BUY_ORDER_STATE_PROCESSING',
+ SET_BUY_ORDER_STATE_FAILURE = 'SET_BUY_ORDER_STATE_FAILURE',
+ SET_BUY_ORDER_STATE_SUCCESS = 'SET_BUY_ORDER_STATE_SUCCESS',
UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE',
UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET',
SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING',
@@ -40,7 +44,12 @@ export const actions = {
updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price),
updateSelectedAssetAmount: (amount?: BigNumberInput) =>
createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount),
- updateBuyOrderState: (orderState: OrderState) => createAction(ActionTypes.UPDATE_BUY_ORDER_STATE, orderState),
+ setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE),
+ setBuyOrderStateValidating: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_VALIDATING),
+ setBuyOrderStateProcessing: (txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) =>
+ createAction(ActionTypes.SET_BUY_ORDER_STATE_PROCESSING, { txHash, startTimeUnix, expectedEndTimeUnix }),
+ setBuyOrderStateFailure: (txHash: string) => createAction(ActionTypes.SET_BUY_ORDER_STATE_FAILURE, txHash),
+ setBuyOrderStateSuccess: (txHash: string) => createAction(ActionTypes.SET_BUY_ORDER_STATE_SUCCESS, txHash),
updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote),
updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData),
setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING),
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
index dd9403052..a3f38c880 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -83,11 +83,71 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.FAILURE,
};
- case ActionTypes.UPDATE_BUY_ORDER_STATE:
+ case ActionTypes.SET_BUY_ORDER_STATE_NONE:
return {
...state,
- buyOrderState: action.data,
+ buyOrderState: { processState: OrderProcessState.NONE },
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING:
+ return {
+ ...state,
+ buyOrderState: { processState: OrderProcessState.VALIDATING },
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING:
+ const processingData = action.data;
+ const { startTimeUnix, expectedEndTimeUnix } = processingData;
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.PROCESSING,
+ txHash: processingData.txHash,
+ progress: {
+ startTimeUnix,
+ expectedEndTimeUnix,
+ ended: false,
+ },
+ },
};
+ case ActionTypes.SET_BUY_ORDER_STATE_FAILURE:
+ const failureTxHash = action.data;
+ if ('txHash' in state.buyOrderState) {
+ if (state.buyOrderState.txHash === failureTxHash) {
+ const failureProgress = {
+ startTimeUnix: state.buyOrderState.progress.startTimeUnix,
+ expectedEndTimeUnix: state.buyOrderState.progress.expectedEndTimeUnix,
+ ended: true,
+ };
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.FAILURE,
+ txHash: state.buyOrderState.txHash,
+ progress: failureProgress,
+ },
+ };
+ }
+ }
+ return state;
+ case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS:
+ const successTxHash = action.data;
+ if ('txHash' in state.buyOrderState) {
+ if (state.buyOrderState.txHash === successTxHash) {
+ const successProgress = {
+ startTimeUnix: state.buyOrderState.progress.startTimeUnix,
+ expectedEndTimeUnix: state.buyOrderState.progress.expectedEndTimeUnix,
+ ended: true,
+ };
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.SUCCESS,
+ txHash: state.buyOrderState.txHash,
+ progress: successProgress,
+ },
+ };
+ }
+ }
+ return state;
case ActionTypes.SET_ERROR_MESSAGE:
return {
...state,
@@ -127,8 +187,6 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
buyOrderState: { processState: OrderProcessState.NONE },
selectedAssetAmount: undefined,
};
- default:
- return state;
}
};
diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts
index 336465e43..34893676d 100644
--- a/packages/instant/src/types.ts
+++ b/packages/instant/src/types.ts
@@ -16,12 +16,19 @@ export enum OrderProcessState {
FAILURE = 'Failure',
}
+export interface SimulatedProgress {
+ startTimeUnix: number;
+ expectedEndTimeUnix: number;
+ ended: boolean;
+}
+
interface OrderStatePreTx {
processState: OrderProcessState.NONE | OrderProcessState.VALIDATING;
}
interface OrderStatePostTx {
processState: OrderProcessState.PROCESSING | OrderProcessState.SUCCESS | OrderProcessState.FAILURE;
txHash: string;
+ progress: SimulatedProgress;
}
export type OrderState = OrderStatePreTx | OrderStatePostTx;
diff --git a/packages/instant/src/util/gas_price_estimator.ts b/packages/instant/src/util/gas_price_estimator.ts
index 336c4a3fa..6b15809a3 100644
--- a/packages/instant/src/util/gas_price_estimator.ts
+++ b/packages/instant/src/util/gas_price_estimator.ts
@@ -1,6 +1,11 @@
import { BigNumber, fetchAsync } from '@0x/utils';
-import { DEFAULT_GAS_PRICE, ETH_GAS_STATION_API_BASE_URL, GWEI_IN_WEI } from '../constants';
+import {
+ DEFAULT_ESTIMATED_TRANSACTION_TIME_MS,
+ DEFAULT_GAS_PRICE,
+ ETH_GAS_STATION_API_BASE_URL,
+ GWEI_IN_WEI,
+} from '../constants';
interface EthGasStationResult {
average: number;
@@ -16,18 +21,25 @@ interface EthGasStationResult {
safeLow: number;
}
-const fetchFastAmountInWeiAsync = async () => {
+interface GasInfo {
+ gasPriceInWei: BigNumber;
+ estimatedTimeMs: number;
+}
+
+const fetchFastAmountInWeiAsync = async (): Promise<GasInfo> => {
const res = await fetchAsync(`${ETH_GAS_STATION_API_BASE_URL}/json/ethgasAPI.json`);
const gasInfo = (await res.json()) as EthGasStationResult;
// Eth Gas Station result is gwei * 10
const gasPriceInGwei = new BigNumber(gasInfo.fast / 10);
- return gasPriceInGwei.mul(GWEI_IN_WEI);
+ // Time is in minutes
+ const estimatedTimeMs = gasInfo.fastWait * 60 * 1000; // Minutes to MS
+ return { gasPriceInWei: gasPriceInGwei.mul(GWEI_IN_WEI), estimatedTimeMs };
};
export class GasPriceEstimator {
- private _lastFetched?: BigNumber;
- public async getFastAmountInWeiAsync(): Promise<BigNumber> {
- let fetchedAmount: BigNumber | undefined;
+ private _lastFetched?: GasInfo;
+ public async getGasInfoAsync(): Promise<GasInfo> {
+ let fetchedAmount: GasInfo | undefined;
try {
fetchedAmount = await fetchFastAmountInWeiAsync();
} catch {
@@ -38,7 +50,13 @@ export class GasPriceEstimator {
this._lastFetched = fetchedAmount;
}
- return fetchedAmount || this._lastFetched || DEFAULT_GAS_PRICE;
+ return (
+ fetchedAmount ||
+ this._lastFetched || {
+ gasPriceInWei: DEFAULT_GAS_PRICE,
+ estimatedTimeMs: DEFAULT_ESTIMATED_TRANSACTION_TIME_MS,
+ }
+ );
}
}
export const gasPriceEstimator = new GasPriceEstimator();
diff --git a/packages/instant/src/util/time.ts b/packages/instant/src/util/time.ts
new file mode 100644
index 000000000..bfe69cad5
--- /dev/null
+++ b/packages/instant/src/util/time.ts
@@ -0,0 +1,39 @@
+const secondsToMinutesAndRemainingSeconds = (seconds: number): { minutes: number; remainingSeconds: number } => {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds - minutes * 60;
+
+ return {
+ minutes,
+ remainingSeconds,
+ };
+};
+
+const padZero = (aNumber: number): string => {
+ return aNumber < 10 ? `0${aNumber}` : aNumber.toString();
+};
+
+export const timeUtil = {
+ // converts seconds to human readable version of seconds or minutes
+ secondsToHumanDescription: (seconds: number): string => {
+ const { minutes, remainingSeconds } = secondsToMinutesAndRemainingSeconds(seconds);
+
+ if (minutes === 0) {
+ const suffix = seconds > 1 ? 's' : '';
+ return `${seconds} second${suffix}`;
+ }
+
+ const minuteSuffix = minutes > 1 ? 's' : '';
+ const minuteText = `${minutes} minute${minuteSuffix}`;
+
+ const secondsSuffix = remainingSeconds > 1 ? 's' : '';
+ const secondsText = remainingSeconds === 0 ? '' : ` ${remainingSeconds} second${secondsSuffix}`;
+
+ return `${minuteText}${secondsText}`;
+ },
+ // converts seconds to stopwatch time (i.e. 05:30 and 00:30)
+ // only goes up to minutes, not hours
+ secondsToStopwatchTime: (seconds: number): string => {
+ const { minutes, remainingSeconds } = secondsToMinutesAndRemainingSeconds(seconds);
+ return `${padZero(minutes)}:${padZero(remainingSeconds)}`;
+ },
+};
diff --git a/packages/instant/test/util/time.test.ts b/packages/instant/test/util/time.test.ts
new file mode 100644
index 000000000..7165761de
--- /dev/null
+++ b/packages/instant/test/util/time.test.ts
@@ -0,0 +1,48 @@
+import { timeUtil } from '../../src/util/time';
+
+describe('assetDataUtil', () => {
+ describe('secondsToHumanDescription', () => {
+ const numsToResults: {
+ [aNumber: number]: string;
+ } = {
+ 1: '1 second',
+ 59: '59 seconds',
+ 60: '1 minute',
+ 119: '1 minute 59 seconds',
+ 120: '2 minutes',
+ 121: '2 minutes 1 second',
+ 122: '2 minutes 2 seconds',
+ };
+
+ const nums = Object.keys(numsToResults);
+ nums.forEach(aNum => {
+ const numInt = parseInt(aNum, 10);
+ it(`should work for ${aNum} seconds`, () => {
+ const expectedResult = numsToResults[numInt];
+ expect(timeUtil.secondsToHumanDescription(numInt)).toEqual(expectedResult);
+ });
+ });
+ });
+ describe('secondsToStopwatchTime', () => {
+ const numsToResults: {
+ [aNumber: number]: string;
+ } = {
+ 1: '00:01',
+ 59: '00:59',
+ 60: '01:00',
+ 119: '01:59',
+ 120: '02:00',
+ 121: '02:01',
+ 2701: '45:01',
+ };
+
+ const nums = Object.keys(numsToResults);
+ nums.forEach(aNum => {
+ const numInt = parseInt(aNum, 10);
+ it(`should work for ${aNum} seconds`, () => {
+ const expectedResult = numsToResults[numInt];
+ expect(timeUtil.secondsToStopwatchTime(numInt)).toEqual(expectedResult);
+ });
+ });
+ });
+});
diff --git a/packages/instant/tslint.json b/packages/instant/tslint.json
index 08b76be97..d43ee8da7 100644
--- a/packages/instant/tslint.json
+++ b/packages/instant/tslint.json
@@ -3,6 +3,7 @@
"rules": {
"custom-no-magic-numbers": false,
"semicolon": [true, "always", "ignore-bound-class-methods"],
- "max-classes-per-file": false
+ "max-classes-per-file": false,
+ "switch-default": false
}
}