aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrandon Millman <brandon@0xproject.com>2018-11-03 03:43:53 +0800
committerGitHub <noreply@github.com>2018-11-03 03:43:53 +0800
commitec83a1d9e735573fe51df7a5854424c0ad4101fb (patch)
treef28def28bee9925daf8dd859b750c94c0cd83f98
parente7e9c2a2ebf32ed96e859c9c50a5c9614e372bc7 (diff)
parentb895b855cbb555736be838e2630890ab6a69d884 (diff)
downloaddexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar.gz
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar.bz2
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar.lz
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar.xz
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.tar.zst
dexon-0x-contracts-ec83a1d9e735573fe51df7a5854424c0ad4101fb.zip
Merge pull request #1207 from 0xProject/feature/instant/affiliate-fee
[instant][asset-buyer] Implement affiliateInfo prop
-rw-r--r--packages/asset-buyer/CHANGELOG.json10
-rw-r--r--packages/asset-buyer/src/asset_buyer.ts33
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts2
-rw-r--r--packages/instant/public/index.html10
-rw-r--r--packages/instant/src/components/buy_button.tsx13
-rw-r--r--packages/instant/src/components/buy_order_state_buttons.tsx4
-rw-r--r--packages/instant/src/components/zero_ex_instant_provider.tsx6
-rw-r--r--packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts4
-rw-r--r--packages/instant/src/containers/selected_erc20_asset_amount_input.ts28
-rw-r--r--packages/instant/src/index.umd.ts3
-rw-r--r--packages/instant/src/redux/reducer.ts3
-rw-r--r--packages/instant/src/types.ts5
-rw-r--r--packages/instant/src/util/assert.ts10
13 files changed, 103 insertions, 28 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json
index 6ba2a0fd9..0d71bb84d 100644
--- a/packages/asset-buyer/CHANGELOG.json
+++ b/packages/asset-buyer/CHANGELOG.json
@@ -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
}
]
},
diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts
index 49743404f..934410c55 100644
--- a/packages/asset-buyer/src/asset_buyer.ts
+++ b/packages/asset-buyer/src/asset_buyer.ts
@@ -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;
diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
index f94ab3fa4..6a67ed1ed 100644
--- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts
+++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
@@ -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
diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html
index 7580ab132..96a703224 100644
--- a/packages/instant/public/index.html
+++ b/packages/instant/public/index.html
@@ -77,11 +77,21 @@
onClose: () => { console.log('0x Instant Closed') }
}
const liquiditySourceOverride = queryParams.getQueryParamValue('liquiditySource');
+ const feeRecipientOverride = queryParams.getQueryParamValue('feeRecipient');
+ const feePercentageOverride = +queryParams.getQueryParamValue('feePercentage');
+ let affiliateInfoOverride;
+ if (feeRecipientOverride !== undefined && feePercentageOverride !== undefined) {
+ affiliateInfoOverride = {
+ feeRecipient: feeRecipientOverride,
+ feePercentage: feePercentageOverride
+ };
+ }
const renderOptionsOverrides = {
liquiditySource: liquiditySourceOverride === 'provided' ? providedOrders : liquiditySourceOverride,
assetData: queryParams.getQueryParamValue('assetData'),
networkId: +queryParams.getQueryParamValue('networkId') || undefined,
defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined,
+ affiliateInfo: affiliateInfoOverride,
}
const renderOptions = Object.assign({}, renderOptionsDefaults, removeUndefined(renderOptionsOverrides));
zeroExInstant.render(renderOptions);
diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx
index c00b1678d..12ac62601 100644
--- a/packages/instant/src/components/buy_button.tsx
+++ b/packages/instant/src/components/buy_button.tsx
@@ -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) {
diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx
index 1d02f8cd9..5c074a67a 100644
--- a/packages/instant/src/components/buy_order_state_buttons.tsx
+++ b/packages/instant/src/components/buy_order_state_buttons.tsx
@@ -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}
diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx
index a7aecab9c..fce03a280 100644
--- a/packages/instant/src/components/zero_ex_instant_provider.tsx
+++ b/packages/instant/src/components/zero_ex_instant_provider.tsx
@@ -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;
}
diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
index 7c36fa4d0..72d99f844 100644
--- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
+++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts
@@ -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 &&
diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
index f0e792e2f..7859261dd 100644
--- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
+++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts
@@ -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,
};
diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts
index b12e65485..806187a16 100644
--- a/packages/instant/src/index.umd.ts
+++ b/packages/instant/src/index.umd.ts
@@ -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;
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
index cf24f8488..d1537b49b 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -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 => {
diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts
index 288a6d111..82dc77d2e 100644
--- a/packages/instant/src/types.ts
+++ b/packages/instant/src/types.ts
@@ -80,3 +80,8 @@ export enum ZeroExInstantError {
AssetMetaDataNotAvailable = 'ASSET_META_DATA_NOT_AVAILABLE',
InsufficientETH = 'INSUFFICIENT_ETH',
}
+
+export interface AffiliateInfo {
+ feeRecipient: string;
+ feePercentage: number;
+}
diff --git a/packages/instant/src/util/assert.ts b/packages/instant/src/util/assert.ts
index 584d3d4b1..20f8ddaee 100644
--- a/packages/instant/src/util/assert.ts
+++ b/packages/instant/src/util/assert.ts
@@ -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}`,
+ );
+ },
};