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`;
|
||||
}
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
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