feat(instant): implement affiliateFeeInfo prop
This commit is contained in:
@@ -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,6 +17,7 @@ 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -58,8 +60,13 @@ export class BuyButton extends React.Component<BuyButtonProps> {
|
||||
|
||||
let txHash: string | undefined;
|
||||
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
|
||||
const feeRecipient = oc(affiliateInfo).feeRecipient();
|
||||
try {
|
||||
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { takerAddress, gasPrice: gasInfo.gasPriceInWei });
|
||||
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, {
|
||||
feeRecipient,
|
||||
takerAddress,
|
||||
gasPrice: gasInfo.gasPriceInWei,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message === AssetBuyerError.SignatureRequestDenied) {
|
||||
|
||||
@@ -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,6 +13,7 @@ export interface BuyOrderStateButtonProps {
|
||||
buyQuote?: BuyQuote;
|
||||
buyOrderProcessingState: OrderProcessState;
|
||||
assetBuyer?: AssetBuyer;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
onViewTransaction: () => void;
|
||||
onValidationPending: (buyQuote: BuyQuote) => void;
|
||||
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
|
||||
@@ -50,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}
|
||||
|
||||
@@ -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';
|
||||
@@ -29,9 +29,10 @@ export interface ZeroExInstantProviderRequiredProps {
|
||||
}
|
||||
|
||||
export interface ZeroExInstantProviderOptionalProps {
|
||||
defaultAssetBuyAmount?: number;
|
||||
defaultAssetBuyAmount: number;
|
||||
additionalAssetMetaDataMap: ObjectMap<AssetMetaData>;
|
||||
networkId: Network;
|
||||
affiliateInfo: AffiliateInfo;
|
||||
}
|
||||
|
||||
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
|
||||
@@ -66,6 +67,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
|
||||
? state.selectedAssetAmount
|
||||
: new BigNumberInput(props.defaultAssetBuyAmount),
|
||||
assetMetaDataMap: completeAssetMetaDataMap,
|
||||
affiliateInfo: props.affiliateInfo,
|
||||
};
|
||||
return storeStateFromProps;
|
||||
}
|
||||
|
||||
@@ -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, ZeroExInstantError } from '../types';
|
||||
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
import { etherscanUtil } from '../util/etherscan';
|
||||
|
||||
@@ -15,6 +15,7 @@ interface ConnectedState {
|
||||
buyQuote?: BuyQuote;
|
||||
buyOrderProcessingState: OrderProcessState;
|
||||
assetBuyer?: AssetBuyer;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
onViewTransaction: () => 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 &&
|
||||
|
||||
@@ -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';
|
||||
@@ -27,10 +28,16 @@ interface ConnectedState {
|
||||
value?: BigNumberInput;
|
||||
asset?: ERC20Asset;
|
||||
isDisabled: boolean;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
}
|
||||
|
||||
interface ConnectedDispatch {
|
||||
updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumberInput, asset?: ERC20Asset) => void;
|
||||
updateBuyQuote: (
|
||||
assetBuyer?: AssetBuyer,
|
||||
value?: BigNumberInput,
|
||||
asset?: ERC20Asset,
|
||||
affiliateInfo?: AffiliateInfo,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
@@ -60,6 +67,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP
|
||||
value: state.selectedAssetAmount,
|
||||
asset: selectedAsset as ERC20Asset,
|
||||
isDisabled,
|
||||
affiliateInfo: state.affiliateInfo,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -68,6 +76,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);
|
||||
@@ -75,9 +84,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;
|
||||
@@ -93,7 +103,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
|
||||
@@ -108,7 +122,7 @@ 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.
|
||||
@@ -120,7 +134,7 @@ const mapDispatchToProps = (
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -135,7 +149,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,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,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;
|
||||
|
||||
@@ -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 = {
|
||||
@@ -43,6 +45,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 => {
|
||||
|
||||
@@ -80,3 +80,8 @@ export enum ZeroExInstantError {
|
||||
AssetMetaDataNotAvailable = 'ASSET_META_DATA_NOT_AVAILABLE',
|
||||
InsufficientETH = 'INSUFFICIENT_ETH',
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -41,4 +41,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}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user