Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/instant/maker-asset-datas-interface
This commit is contained in:
		@@ -14,6 +14,16 @@
 | 
			
		||||
            {
 | 
			
		||||
                "note": "No longer require that provided orders all have the same maker and taker asset data",
 | 
			
		||||
                "pr": 1197
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note":
 | 
			
		||||
                    "Fix bug where `BuyQuoteInfo` objects could return `totalEthAmount` and `feeEthAmount` that were not whole numbers",
 | 
			
		||||
                "pr": 1207
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note":
 | 
			
		||||
                    "Fix bug where default values for `AssetBuyer` public facing methods could get overriden by `undefined` values",
 | 
			
		||||
                "pr": 1207
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -90,10 +90,11 @@ export class AssetBuyer {
 | 
			
		||||
     * @return  An instance of AssetBuyer
 | 
			
		||||
     */
 | 
			
		||||
    constructor(provider: Provider, orderProvider: OrderProvider, options: Partial<AssetBuyerOpts> = {}) {
 | 
			
		||||
        const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = {
 | 
			
		||||
            ...constants.DEFAULT_ASSET_BUYER_OPTS,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
        const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = _.merge(
 | 
			
		||||
            {},
 | 
			
		||||
            constants.DEFAULT_ASSET_BUYER_OPTS,
 | 
			
		||||
            options,
 | 
			
		||||
        );
 | 
			
		||||
        assert.isWeb3Provider('provider', provider);
 | 
			
		||||
        assert.isValidOrderProvider('orderProvider', orderProvider);
 | 
			
		||||
        assert.isNumber('networkId', networkId);
 | 
			
		||||
@@ -122,10 +123,11 @@ export class AssetBuyer {
 | 
			
		||||
        assetBuyAmount: BigNumber,
 | 
			
		||||
        options: Partial<BuyQuoteRequestOpts> = {},
 | 
			
		||||
    ): Promise<BuyQuote> {
 | 
			
		||||
        const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = {
 | 
			
		||||
            ...constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
        const { feePercentage, shouldForceOrderRefresh, slippagePercentage } = _.merge(
 | 
			
		||||
            {},
 | 
			
		||||
            constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
 | 
			
		||||
            options,
 | 
			
		||||
        );
 | 
			
		||||
        assert.isString('assetData', assetData);
 | 
			
		||||
        assert.isBigNumber('assetBuyAmount', assetBuyAmount);
 | 
			
		||||
        assert.isValidPercentage('feePercentage', feePercentage);
 | 
			
		||||
@@ -186,10 +188,11 @@ export class AssetBuyer {
 | 
			
		||||
        buyQuote: BuyQuote,
 | 
			
		||||
        options: Partial<BuyQuoteExecutionOpts> = {},
 | 
			
		||||
    ): Promise<string> {
 | 
			
		||||
        const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = {
 | 
			
		||||
            ...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
 | 
			
		||||
            ...options,
 | 
			
		||||
        };
 | 
			
		||||
        const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = _.merge(
 | 
			
		||||
            {},
 | 
			
		||||
            constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
 | 
			
		||||
            options,
 | 
			
		||||
        );
 | 
			
		||||
        assert.isValidBuyQuote('buyQuote', buyQuote);
 | 
			
		||||
        if (!_.isUndefined(ethAmount)) {
 | 
			
		||||
            assert.isBigNumber('ethAmount', ethAmount);
 | 
			
		||||
@@ -198,6 +201,12 @@ export class AssetBuyer {
 | 
			
		||||
            assert.isETHAddressHex('takerAddress', takerAddress);
 | 
			
		||||
        }
 | 
			
		||||
        assert.isETHAddressHex('feeRecipient', feeRecipient);
 | 
			
		||||
        if (!_.isUndefined(gasLimit)) {
 | 
			
		||||
            assert.isNumber('gasLimit', gasLimit);
 | 
			
		||||
        }
 | 
			
		||||
        if (!_.isUndefined(gasPrice)) {
 | 
			
		||||
            assert.isBigNumber('gasPrice', gasPrice);
 | 
			
		||||
        }
 | 
			
		||||
        const { orders, feeOrders, feePercentage, assetBuyAmount, worstCaseQuoteInfo } = buyQuote;
 | 
			
		||||
        // if no takerAddress is provided, try to get one from the provider
 | 
			
		||||
        let finalTakerAddress;
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,7 @@ function calculateQuoteInfo(
 | 
			
		||||
        ethAmountToBuyZrx = findEthAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
 | 
			
		||||
    }
 | 
			
		||||
    /// find the eth amount needed to buy the affiliate fee
 | 
			
		||||
    const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage);
 | 
			
		||||
    const ethAmountToBuyAffiliateFee = ethAmountToBuyAsset.mul(feePercentage).ceil();
 | 
			
		||||
    const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyZrx);
 | 
			
		||||
    const ethAmountTotal = totalEthAmountWithoutAffiliateFee.plus(ethAmountToBuyAffiliateFee);
 | 
			
		||||
    // divide into the assetBuyAmount in order to find rate of makerAsset / WETH
 | 
			
		||||
 
 | 
			
		||||
@@ -77,12 +77,22 @@
 | 
			
		||||
        }
 | 
			
		||||
        const orderSourceOverride = queryParams.getQueryParamValue('orderSource');
 | 
			
		||||
        const availableAssetDatasString = queryParams.getQueryParamValue('availableAssetDatas');
 | 
			
		||||
        const feeRecipientOverride = queryParams.getQueryParamValue('feeRecipient');
 | 
			
		||||
        const feePercentageOverride = +queryParams.getQueryParamValue('feePercentage');
 | 
			
		||||
        let affiliateInfoOverride;
 | 
			
		||||
        if (feeRecipientOverride !== undefined && feePercentageOverride !== undefined) {
 | 
			
		||||
            affiliateInfoOverride = {
 | 
			
		||||
                feeRecipient: feeRecipientOverride,
 | 
			
		||||
                feePercentage: feePercentageOverride
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        const renderOptionsOverrides = {
 | 
			
		||||
            orderSource: orderSourceOverride === 'provided' ? [providedOrder] : orderSourceOverride,
 | 
			
		||||
            networkId: +queryParams.getQueryParamValue('networkId') || undefined,
 | 
			
		||||
            defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined,
 | 
			
		||||
            availableAssetDatas: availableAssetDatasString ? JSON.parse(availableAssetDatasString) : undefined,
 | 
			
		||||
            defaultSelectedAssetData: queryParams.getQueryParamValue('defaultSelectedAssetData'),
 | 
			
		||||
            affiliateInfo: affiliateInfoOverride,
 | 
			
		||||
        }
 | 
			
		||||
        const renderOptions = Object.assign({}, renderOptionsDefaults, removeUndefined(renderOptionsOverrides));
 | 
			
		||||
        zeroExInstant.render(renderOptions);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { oc } from 'ts-optchain';
 | 
			
		||||
 | 
			
		||||
import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
 | 
			
		||||
import { ColorOption } from '../style/theme';
 | 
			
		||||
import { ZeroExInstantError } from '../types';
 | 
			
		||||
import { AffiliateInfo, ZeroExInstantError } from '../types';
 | 
			
		||||
import { getBestAddress } from '../util/address';
 | 
			
		||||
import { balanceUtil } from '../util/balance';
 | 
			
		||||
import { gasPriceEstimator } from '../util/gas_price_estimator';
 | 
			
		||||
@@ -16,10 +17,11 @@ import { Button, Text } from './ui';
 | 
			
		||||
export interface BuyButtonProps {
 | 
			
		||||
    buyQuote?: BuyQuote;
 | 
			
		||||
    assetBuyer?: AssetBuyer;
 | 
			
		||||
    affiliateInfo?: AffiliateInfo;
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +44,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
 | 
			
		||||
    }
 | 
			
		||||
    private readonly _handleClick = async () => {
 | 
			
		||||
        // The button is disabled when there is no buy quote anyway.
 | 
			
		||||
        const { buyQuote, assetBuyer } = this.props;
 | 
			
		||||
        const { buyQuote, assetBuyer, affiliateInfo } = this.props;
 | 
			
		||||
        if (_.isUndefined(buyQuote) || _.isUndefined(assetBuyer)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -57,9 +59,14 @@ export class BuyButton extends React.Component<BuyButtonProps> {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let txHash: string | undefined;
 | 
			
		||||
        const gasPrice = await gasPriceEstimator.getFastAmountInWeiAsync();
 | 
			
		||||
        const gasInfo = await gasPriceEstimator.getGasInfoAsync();
 | 
			
		||||
        const feeRecipient = oc(affiliateInfo).feeRecipient();
 | 
			
		||||
        try {
 | 
			
		||||
            txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { takerAddress, gasPrice });
 | 
			
		||||
            txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, {
 | 
			
		||||
                feeRecipient,
 | 
			
		||||
                takerAddress,
 | 
			
		||||
                gasPrice: gasInfo.gasPriceInWei,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            if (e instanceof Error) {
 | 
			
		||||
                if (e.message === AssetBuyerError.SignatureRequestDenied) {
 | 
			
		||||
@@ -73,7 +80,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 +92,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
 | 
			
		||||
            }
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.props.onBuySuccess(buyQuote, txHash);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								packages/instant/src/components/buy_order_progress.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/instant/src/components/buy_order_progress.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { TimedProgressBar } from '../components/timed_progress_bar';
 | 
			
		||||
 | 
			
		||||
import { TimeCounter } from '../components/time_counter';
 | 
			
		||||
import { Container } from '../components/ui';
 | 
			
		||||
import { OrderProcessState, OrderState } from '../types';
 | 
			
		||||
 | 
			
		||||
export interface BuyOrderProgressProps {
 | 
			
		||||
    buyOrderState: OrderState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = props => {
 | 
			
		||||
    const { buyOrderState } = props;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        buyOrderState.processState === OrderProcessState.PROCESSING ||
 | 
			
		||||
        buyOrderState.processState === OrderProcessState.SUCCESS ||
 | 
			
		||||
        buyOrderState.processState === OrderProcessState.FAILURE
 | 
			
		||||
    ) {
 | 
			
		||||
        const progress = buyOrderState.progress;
 | 
			
		||||
        const hasEnded = buyOrderState.processState !== OrderProcessState.PROCESSING;
 | 
			
		||||
        const expectedTimeMs = progress.expectedEndTimeUnix - progress.startTimeUnix;
 | 
			
		||||
        return (
 | 
			
		||||
            <Container padding="20px 20px 0px 20px" width="100%">
 | 
			
		||||
                <Container marginBottom="5px">
 | 
			
		||||
                    <TimeCounter estimatedTimeMs={expectedTimeMs} hasEnded={hasEnded} key={progress.startTimeUnix} />
 | 
			
		||||
                </Container>
 | 
			
		||||
                <TimedProgressBar expectedTimeMs={expectedTimeMs} hasEnded={hasEnded} key={progress.startTimeUnix} />
 | 
			
		||||
            </Container>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
};
 | 
			
		||||
@@ -2,7 +2,7 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { ColorOption } from '../style/theme';
 | 
			
		||||
import { OrderProcessState, ZeroExInstantError } from '../types';
 | 
			
		||||
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
 | 
			
		||||
 | 
			
		||||
import { BuyButton } from './buy_button';
 | 
			
		||||
import { PlacingOrderButton } from './placing_order_button';
 | 
			
		||||
@@ -13,17 +13,17 @@ export interface BuyOrderStateButtonProps {
 | 
			
		||||
    buyQuote?: BuyQuote;
 | 
			
		||||
    buyOrderProcessingState: OrderProcessState;
 | 
			
		||||
    assetBuyer?: AssetBuyer;
 | 
			
		||||
    affiliateInfo?: AffiliateInfo;
 | 
			
		||||
    onViewTransaction: () => void;
 | 
			
		||||
    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 (
 | 
			
		||||
@@ -51,6 +51,7 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
 | 
			
		||||
        <BuyButton
 | 
			
		||||
            buyQuote={props.buyQuote}
 | 
			
		||||
            assetBuyer={props.assetBuyer}
 | 
			
		||||
            affiliateInfo={props.affiliateInfo}
 | 
			
		||||
            onValidationPending={props.onValidationPending}
 | 
			
		||||
            onValidationFail={props.onValidationFail}
 | 
			
		||||
            onSignatureDenied={props.onSignatureDenied}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								packages/instant/src/components/time_counter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								packages/instant/src/components/time_counter.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { ONE_SECOND_MS } from '../constants';
 | 
			
		||||
import { ColorOption } from '../style/theme';
 | 
			
		||||
import { timeUtil } from '../util/time';
 | 
			
		||||
 | 
			
		||||
import { Container } from './ui/container';
 | 
			
		||||
import { Flex } from './ui/flex';
 | 
			
		||||
import { Text } from './ui/text';
 | 
			
		||||
 | 
			
		||||
export interface TimeCounterProps {
 | 
			
		||||
    estimatedTimeMs: number;
 | 
			
		||||
    hasEnded: boolean;
 | 
			
		||||
}
 | 
			
		||||
interface TimeCounterState {
 | 
			
		||||
    elapsedSeconds: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TimeCounter extends React.Component<TimeCounterProps, TimeCounterState> {
 | 
			
		||||
    public state = {
 | 
			
		||||
        elapsedSeconds: 0,
 | 
			
		||||
    };
 | 
			
		||||
    private _timerId?: number;
 | 
			
		||||
 | 
			
		||||
    public componentDidMount(): void {
 | 
			
		||||
        this._setupTimerBasedOnProps();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentWillUnmount(): void {
 | 
			
		||||
        this._clearTimer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentDidUpdate(prevProps: TimeCounterProps): void {
 | 
			
		||||
        if (prevProps.hasEnded !== this.props.hasEnded) {
 | 
			
		||||
            this._setupTimerBasedOnProps();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public render(): React.ReactNode {
 | 
			
		||||
        const estimatedTimeSeconds = this.props.estimatedTimeMs / ONE_SECOND_MS;
 | 
			
		||||
        return (
 | 
			
		||||
            <Flex justify="space-between">
 | 
			
		||||
                <Container>
 | 
			
		||||
                    <Container marginRight="5px" display="inline">
 | 
			
		||||
                        <Text fontWeight={600} fontColor={ColorOption.grey}>
 | 
			
		||||
                            Est. Time
 | 
			
		||||
                        </Text>
 | 
			
		||||
                    </Container>
 | 
			
		||||
                    <Text fontColor={ColorOption.grey}>
 | 
			
		||||
                        ({timeUtil.secondsToHumanDescription(estimatedTimeSeconds)})
 | 
			
		||||
                    </Text>
 | 
			
		||||
                </Container>
 | 
			
		||||
                <Text fontColor={ColorOption.grey}>
 | 
			
		||||
                    Time: {timeUtil.secondsToStopwatchTime(this.state.elapsedSeconds)}
 | 
			
		||||
                </Text>
 | 
			
		||||
            </Flex>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private _setupTimerBasedOnProps(): void {
 | 
			
		||||
        this.props.hasEnded ? this._clearTimer() : this._newTimer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private _newTimer(): void {
 | 
			
		||||
        this._clearTimer();
 | 
			
		||||
        this._timerId = window.setInterval(() => {
 | 
			
		||||
            this.setState({
 | 
			
		||||
                elapsedSeconds: this.state.elapsedSeconds + 1,
 | 
			
		||||
            });
 | 
			
		||||
        }, ONE_SECOND_MS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private _clearTimer(): void {
 | 
			
		||||
        if (this._timerId) {
 | 
			
		||||
            window.clearInterval(this._timerId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								packages/instant/src/components/timed_progress_bar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								packages/instant/src/components/timed_progress_bar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
 | 
			
		||||
import { PROGRESS_FINISH_ANIMATION_TIME_MS, PROGRESS_STALL_AT_WIDTH } from '../constants';
 | 
			
		||||
import { ColorOption, keyframes, styled } from '../style/theme';
 | 
			
		||||
 | 
			
		||||
import { Container } from './ui/container';
 | 
			
		||||
 | 
			
		||||
export interface TimedProgressBarProps {
 | 
			
		||||
    expectedTimeMs: number;
 | 
			
		||||
    hasEnded: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Timed Progress Bar
 | 
			
		||||
 * Goes from 0% -> PROGRESS_STALL_AT_WIDTH over time of expectedTimeMs
 | 
			
		||||
 * When hasEnded set to true, goes to 100% through animation of PROGRESS_FINISH_ANIMATION_TIME_MS length of time
 | 
			
		||||
 */
 | 
			
		||||
export class TimedProgressBar extends React.Component<TimedProgressBarProps, {}> {
 | 
			
		||||
    private readonly _barRef = React.createRef<HTMLDivElement>();
 | 
			
		||||
 | 
			
		||||
    public render(): React.ReactNode {
 | 
			
		||||
        const timedProgressProps = this._calculateTimedProgressProps();
 | 
			
		||||
        return (
 | 
			
		||||
            <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px">
 | 
			
		||||
                <TimedProgress {...timedProgressProps} ref={this._barRef as any} />
 | 
			
		||||
            </Container>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private _calculateTimedProgressProps(): TimedProgressProps {
 | 
			
		||||
        if (this.props.hasEnded) {
 | 
			
		||||
            if (!this._barRef.current) {
 | 
			
		||||
                throw new Error('ended but no reference');
 | 
			
		||||
            }
 | 
			
		||||
            const fromWidth = `${this._barRef.current.offsetWidth}px`;
 | 
			
		||||
            return {
 | 
			
		||||
                timeMs: PROGRESS_FINISH_ANIMATION_TIME_MS,
 | 
			
		||||
                fromWidth,
 | 
			
		||||
                toWidth: '100%',
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            timeMs: this.props.expectedTimeMs,
 | 
			
		||||
            fromWidth: '0px',
 | 
			
		||||
            toWidth: PROGRESS_STALL_AT_WIDTH,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const expandingWidthKeyframes = (fromWidth: string, toWidth: string) => {
 | 
			
		||||
    return keyframes`
 | 
			
		||||
          from {
 | 
			
		||||
              width: ${fromWidth};
 | 
			
		||||
          }
 | 
			
		||||
          to {
 | 
			
		||||
              width: ${toWidth};
 | 
			
		||||
          }
 | 
			
		||||
      `;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface TimedProgressProps {
 | 
			
		||||
    timeMs: number;
 | 
			
		||||
    fromWidth: string;
 | 
			
		||||
    toWidth: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TimedProgress =
 | 
			
		||||
    styled.div <
 | 
			
		||||
    TimedProgressProps >
 | 
			
		||||
    `
 | 
			
		||||
    background-color: ${props => props.theme[ColorOption.primaryColor]};
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    height: 6px;
 | 
			
		||||
    animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)}
 | 
			
		||||
      ${props => props.timeMs}ms linear 1 forwards;
 | 
			
		||||
  `;
 | 
			
		||||
@@ -5,13 +5,15 @@ import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order
 | 
			
		||||
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 { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress';
 | 
			
		||||
 | 
			
		||||
import { ColorOption } from '../style/theme';
 | 
			
		||||
import { zIndex } from '../style/z_index';
 | 
			
		||||
 | 
			
		||||
import { SlideAnimationState } from './animations/slide_animation';
 | 
			
		||||
import { SlidingPanel } from './sliding_panel';
 | 
			
		||||
import { Container, Flex } from './ui';
 | 
			
		||||
 | 
			
		||||
export interface ZeroExInstantContainerProps {}
 | 
			
		||||
export interface ZeroExInstantContainerState {
 | 
			
		||||
    tokenSelectionPanelAnimationState: SlideAnimationState;
 | 
			
		||||
@@ -37,6 +39,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
 | 
			
		||||
                >
 | 
			
		||||
                    <Flex direction="column" justify="flex-start">
 | 
			
		||||
                        <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} />
 | 
			
		||||
                        <SelectedAssetBuyOrderProgress />
 | 
			
		||||
                        <LatestBuyQuoteOrderDetails />
 | 
			
		||||
                        <Container padding="20px" width="100%">
 | 
			
		||||
                            <SelectedAssetBuyOrderStateButtons />
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { asyncData } from '../redux/async_data';
 | 
			
		||||
import { INITIAL_STATE, State } from '../redux/reducer';
 | 
			
		||||
import { store, Store } from '../redux/store';
 | 
			
		||||
import { fonts } from '../style/fonts';
 | 
			
		||||
import { AssetMetaData, Network } from '../types';
 | 
			
		||||
import { AffiliateInfo, AssetMetaData, Network } from '../types';
 | 
			
		||||
import { assetUtils } from '../util/asset';
 | 
			
		||||
import { BigNumberInput } from '../util/big_number_input';
 | 
			
		||||
import { errorFlasher } from '../util/error_flasher';
 | 
			
		||||
@@ -32,6 +32,7 @@ export interface ZeroExInstantProviderOptionalProps {
 | 
			
		||||
    defaultSelectedAssetData: string;
 | 
			
		||||
    additionalAssetMetaDataMap: ObjectMap<AssetMetaData>;
 | 
			
		||||
    networkId: Network;
 | 
			
		||||
    affiliateInfo: AffiliateInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
 | 
			
		||||
@@ -76,6 +77,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
 | 
			
		||||
                ? undefined
 | 
			
		||||
                : assetUtils.createAssetsFromAssetDatas(props.availableAssetDatas, completeAssetMetaDataMap, networkId),
 | 
			
		||||
            assetMetaDataMap: completeAssetMetaDataMap,
 | 
			
		||||
            affiliateInfo: props.affiliateInfo,
 | 
			
		||||
        };
 | 
			
		||||
        return storeStateFromProps;
 | 
			
		||||
    }
 | 
			
		||||
@@ -98,7 +100,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
 | 
			
		||||
        // 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();
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,11 @@ export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer';
 | 
			
		||||
export const INJECTED_DIV_ID = 'zeroExInstant';
 | 
			
		||||
export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed';
 | 
			
		||||
export const GWEI_IN_WEI = new BigNumber(1000000000);
 | 
			
		||||
export const ONE_SECOND_MS = 1000;
 | 
			
		||||
export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
 | 
			
		||||
export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6);
 | 
			
		||||
export const DEFAULT_ESTIMATED_TRANSACTION_TIME_MS = ONE_MINUTE_MS * 2;
 | 
			
		||||
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_STALL_AT_WIDTH = '95%';
 | 
			
		||||
export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { BuyOrderProgress } from '../components/buy_order_progress';
 | 
			
		||||
import { State } from '../redux/reducer';
 | 
			
		||||
import { OrderState } from '../types';
 | 
			
		||||
 | 
			
		||||
interface ConnectedState {
 | 
			
		||||
    buyOrderState: OrderState;
 | 
			
		||||
}
 | 
			
		||||
const mapStateToProps = (state: State, _ownProps: {}): ConnectedState => ({
 | 
			
		||||
    buyOrderState: state.buyOrderState,
 | 
			
		||||
});
 | 
			
		||||
export const SelectedAssetBuyOrderProgress = connect(mapStateToProps)(BuyOrderProgress);
 | 
			
		||||
@@ -7,7 +7,7 @@ import { Dispatch } from 'redux';
 | 
			
		||||
import { BuyOrderStateButtons } from '../components/buy_order_state_buttons';
 | 
			
		||||
import { Action, actions } from '../redux/actions';
 | 
			
		||||
import { State } from '../redux/reducer';
 | 
			
		||||
import { OrderProcessState, OrderState, ZeroExInstantError } from '../types';
 | 
			
		||||
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
 | 
			
		||||
import { errorFlasher } from '../util/error_flasher';
 | 
			
		||||
import { etherscanUtil } from '../util/etherscan';
 | 
			
		||||
 | 
			
		||||
@@ -15,13 +15,14 @@ interface ConnectedState {
 | 
			
		||||
    buyQuote?: BuyQuote;
 | 
			
		||||
    buyOrderProcessingState: OrderProcessState;
 | 
			
		||||
    assetBuyer?: AssetBuyer;
 | 
			
		||||
    affiliateInfo?: AffiliateInfo;
 | 
			
		||||
    onViewTransaction: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -32,6 +33,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt
 | 
			
		||||
    buyOrderProcessingState: state.buyOrderState.processState,
 | 
			
		||||
    assetBuyer: state.assetBuyer,
 | 
			
		||||
    buyQuote: state.latestBuyQuote,
 | 
			
		||||
    affiliateInfo: state.affiliateInfo,
 | 
			
		||||
    onViewTransaction: () => {
 | 
			
		||||
        if (
 | 
			
		||||
            state.assetBuyer &&
 | 
			
		||||
@@ -56,24 +58,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);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,13 @@ import * as _ from 'lodash';
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { Dispatch } from 'redux';
 | 
			
		||||
import { oc } from 'ts-optchain';
 | 
			
		||||
 | 
			
		||||
import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input';
 | 
			
		||||
import { Action, actions } from '../redux/actions';
 | 
			
		||||
import { State } from '../redux/reducer';
 | 
			
		||||
import { ColorOption } from '../style/theme';
 | 
			
		||||
import { ERC20Asset, OrderProcessState } from '../types';
 | 
			
		||||
import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types';
 | 
			
		||||
import { assetUtils } from '../util/asset';
 | 
			
		||||
import { BigNumberInput } from '../util/big_number_input';
 | 
			
		||||
import { errorFlasher } from '../util/error_flasher';
 | 
			
		||||
@@ -28,10 +29,16 @@ interface ConnectedState {
 | 
			
		||||
    asset?: ERC20Asset;
 | 
			
		||||
    isDisabled: boolean;
 | 
			
		||||
    numberOfAssetsAvailable?: number;
 | 
			
		||||
    affiliateInfo?: AffiliateInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ConnectedDispatch {
 | 
			
		||||
    updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumberInput, asset?: ERC20Asset) => void;
 | 
			
		||||
    updateBuyQuote: (
 | 
			
		||||
        assetBuyer?: AssetBuyer,
 | 
			
		||||
        value?: BigNumberInput,
 | 
			
		||||
        asset?: ERC20Asset,
 | 
			
		||||
        affiliateInfo?: AffiliateInfo,
 | 
			
		||||
    ) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ConnectedProps {
 | 
			
		||||
@@ -59,6 +66,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP
 | 
			
		||||
        asset: selectedAsset,
 | 
			
		||||
        isDisabled,
 | 
			
		||||
        numberOfAssetsAvailable,
 | 
			
		||||
        affiliateInfo: state.affiliateInfo,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +75,7 @@ const updateBuyQuoteAsync = async (
 | 
			
		||||
    dispatch: Dispatch<Action>,
 | 
			
		||||
    asset: ERC20Asset,
 | 
			
		||||
    assetAmount: BigNumber,
 | 
			
		||||
    affiliateInfo?: AffiliateInfo,
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    // get a new buy quote.
 | 
			
		||||
    const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
 | 
			
		||||
@@ -74,9 +83,10 @@ const updateBuyQuoteAsync = async (
 | 
			
		||||
    // mark quote as pending
 | 
			
		||||
    dispatch(actions.setQuoteRequestStatePending());
 | 
			
		||||
 | 
			
		||||
    const feePercentage = oc(affiliateInfo).feePercentage();
 | 
			
		||||
    let newBuyQuote: BuyQuote | undefined;
 | 
			
		||||
    try {
 | 
			
		||||
        newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue);
 | 
			
		||||
        newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        dispatch(actions.setQuoteRequestStateFailure());
 | 
			
		||||
        let errorMessage;
 | 
			
		||||
@@ -92,7 +102,11 @@ const updateBuyQuoteAsync = async (
 | 
			
		||||
            const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
 | 
			
		||||
            errorMessage = `${assetName} is currently unavailable`;
 | 
			
		||||
        }
 | 
			
		||||
        if (!_.isUndefined(errorMessage)) {
 | 
			
		||||
            errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // We have a successful new buy quote
 | 
			
		||||
@@ -107,19 +121,19 @@ const mapDispatchToProps = (
 | 
			
		||||
    dispatch: Dispatch<Action>,
 | 
			
		||||
    _ownProps: SelectedERC20AssetAmountInputProps,
 | 
			
		||||
): ConnectedDispatch => ({
 | 
			
		||||
    updateBuyQuote: (assetBuyer, value, asset) => {
 | 
			
		||||
    updateBuyQuote: (assetBuyer, value, asset, affiliateInfo) => {
 | 
			
		||||
        // Update the input
 | 
			
		||||
        dispatch(actions.updateSelectedAssetAmount(value));
 | 
			
		||||
        // 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)) {
 | 
			
		||||
        if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) {
 | 
			
		||||
            // even if it's debounced, give them the illusion it's loading
 | 
			
		||||
            dispatch(actions.setQuoteRequestStatePending());
 | 
			
		||||
            // tslint:disable-next-line:no-floating-promises
 | 
			
		||||
            debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value);
 | 
			
		||||
            debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, affiliateInfo);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
@@ -134,7 +148,7 @@ const mergeProps = (
 | 
			
		||||
        asset: connectedState.asset,
 | 
			
		||||
        value: connectedState.value,
 | 
			
		||||
        onChange: (value, asset) => {
 | 
			
		||||
            connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset);
 | 
			
		||||
            connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset, connectedState.affiliateInfo);
 | 
			
		||||
        },
 | 
			
		||||
        isDisabled: connectedState.isDisabled,
 | 
			
		||||
        numberOfAssetsAvailable: connectedState.numberOfAssetsAvailable,
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,9 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA
 | 
			
		||||
    if (!_.isUndefined(props.zIndex)) {
 | 
			
		||||
        assert.isNumber('props.zIndex', props.zIndex);
 | 
			
		||||
    }
 | 
			
		||||
    if (!_.isUndefined(props.affiliateInfo)) {
 | 
			
		||||
        assert.isValidaffiliateInfo('props.affiliateInfo', props.affiliateInfo);
 | 
			
		||||
    }
 | 
			
		||||
    const appendToIfExists = document.querySelector(selector);
 | 
			
		||||
    assert.assert(!_.isNull(appendToIfExists), `Could not find div with selector: ${selector}`);
 | 
			
		||||
    const appendTo = appendToIfExists as Element;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import * as _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { BigNumberInput } from '../util/big_number_input';
 | 
			
		||||
 | 
			
		||||
import { ActionsUnion, Asset, OrderState } from '../types';
 | 
			
		||||
import { ActionsUnion, Asset } 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_AVAILABLE_ASSETS = 'SET_AVAILABLE_ASSETS',
 | 
			
		||||
@@ -41,7 +45,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: (asset: Asset) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, asset),
 | 
			
		||||
    setAvailableAssets: (availableAssets: Asset[]) => createAction(ActionTypes.SET_AVAILABLE_ASSETS, availableAssets),
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import * as _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { assetMetaDataMap } from '../data/asset_meta_data_map';
 | 
			
		||||
import {
 | 
			
		||||
    AffiliateInfo,
 | 
			
		||||
    Asset,
 | 
			
		||||
    AssetMetaData,
 | 
			
		||||
    AsyncProcessState,
 | 
			
		||||
@@ -31,6 +32,7 @@ export interface State {
 | 
			
		||||
    quoteRequestState: AsyncProcessState;
 | 
			
		||||
    latestErrorMessage?: string;
 | 
			
		||||
    latestErrorDisplayStatus: DisplayStatus;
 | 
			
		||||
    affiliateInfo?: AffiliateInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const INITIAL_STATE: State = {
 | 
			
		||||
@@ -44,6 +46,7 @@ export const INITIAL_STATE: State = {
 | 
			
		||||
    latestErrorMessage: undefined,
 | 
			
		||||
    latestErrorDisplayStatus: DisplayStatus.Hidden,
 | 
			
		||||
    quoteRequestState: AsyncProcessState.NONE,
 | 
			
		||||
    affiliateInfo: undefined,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
 | 
			
		||||
@@ -84,11 +87,62 @@ 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,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
        case ActionTypes.SET_BUY_ORDER_STATE_FAILURE:
 | 
			
		||||
            const failureTxHash = action.data;
 | 
			
		||||
            if ('txHash' in state.buyOrderState) {
 | 
			
		||||
                if (state.buyOrderState.txHash === failureTxHash) {
 | 
			
		||||
                    const { txHash, progress } = state.buyOrderState;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...state,
 | 
			
		||||
                        buyOrderState: {
 | 
			
		||||
                            processState: OrderProcessState.FAILURE,
 | 
			
		||||
                            txHash,
 | 
			
		||||
                            progress,
 | 
			
		||||
                        },
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return state;
 | 
			
		||||
        case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS:
 | 
			
		||||
            const successTxHash = action.data;
 | 
			
		||||
            if ('txHash' in state.buyOrderState) {
 | 
			
		||||
                if (state.buyOrderState.txHash === successTxHash) {
 | 
			
		||||
                    const { txHash, progress } = state.buyOrderState;
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...state,
 | 
			
		||||
                        buyOrderState: {
 | 
			
		||||
                            processState: OrderProcessState.SUCCESS,
 | 
			
		||||
                            txHash,
 | 
			
		||||
                            progress,
 | 
			
		||||
                        },
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return state;
 | 
			
		||||
        case ActionTypes.SET_ERROR_MESSAGE:
 | 
			
		||||
            return {
 | 
			
		||||
                ...state,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,12 +16,18 @@ export enum OrderProcessState {
 | 
			
		||||
    FAILURE = 'Failure',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SimulatedProgress {
 | 
			
		||||
    startTimeUnix: number;
 | 
			
		||||
    expectedEndTimeUnix: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
@@ -77,3 +83,8 @@ export enum ZeroExInstantError {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SimpleHandler = () => void;
 | 
			
		||||
 | 
			
		||||
export interface AffiliateInfo {
 | 
			
		||||
    feeRecipient: string;
 | 
			
		||||
    feePercentage: number;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { assetDataUtils } from '@0x/order-utils';
 | 
			
		||||
import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types';
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { AssetMetaData } from '../types';
 | 
			
		||||
import { AffiliateInfo, AssetMetaData } from '../types';
 | 
			
		||||
 | 
			
		||||
export const assert = {
 | 
			
		||||
    ...sharedAssert,
 | 
			
		||||
@@ -44,4 +44,12 @@ export const assert = {
 | 
			
		||||
            assert.isUri(`${variableName}.imageUrl`, metaData.imageUrl);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    isValidaffiliateInfo(variableName: string, affiliateInfo: AffiliateInfo): void {
 | 
			
		||||
        assert.isETHAddressHex(`${variableName}.recipientAddress`, affiliateInfo.feeRecipient);
 | 
			
		||||
        assert.isNumber(`${variableName}.percentage`, affiliateInfo.feePercentage);
 | 
			
		||||
        assert.assert(
 | 
			
		||||
            affiliateInfo.feePercentage >= 0 && affiliateInfo.feePercentage <= 0.05,
 | 
			
		||||
            `Expected ${variableName}.percentage to be between 0 and 0.05, but is ${affiliateInfo.feePercentage}`,
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								packages/instant/src/util/time.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/instant/src/util/time.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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)}`;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										48
									
								
								packages/instant/test/util/time.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/instant/test/util/time.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import { timeUtil } from '../../src/util/time';
 | 
			
		||||
 | 
			
		||||
describe('timeUtil', () => {
 | 
			
		||||
    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);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user