@0x/asset-swapper
: Add affiliate fee support to EP swap quote consumer
This commit is contained in:
parent
d5180d3ebc
commit
341c5782e5
@ -1,6 +1,7 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import {
|
||||
ExchangeProxyContractOpts,
|
||||
ExtensionContractType,
|
||||
ForwarderExtensionContractOpts,
|
||||
OrderPrunerOpts,
|
||||
@ -20,6 +21,7 @@ const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
const MAINNET_CHAIN_ID = 1;
|
||||
const ONE_SECOND_MS = 1000;
|
||||
const DEFAULT_PER_PAGE = 1000;
|
||||
const ZERO_AMOUNT = new BigNumber(0);
|
||||
|
||||
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
||||
expiryBufferMs: 120000, // 2 minutes
|
||||
@ -60,8 +62,23 @@ const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
|
||||
extensionContractOpts: DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS,
|
||||
};
|
||||
|
||||
const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = {
|
||||
isFromETH: false,
|
||||
isToETH: false,
|
||||
affiliateFee: {
|
||||
recipient: NULL_ADDRESS,
|
||||
buyTokenFeeAmount: ZERO_AMOUNT,
|
||||
sellTokenFeeAmount: ZERO_AMOUNT,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
|
||||
|
||||
const DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
|
||||
useExtensionContract: ExtensionContractType.ExchangeProxy,
|
||||
extensionContractOpts: DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
|
||||
};
|
||||
|
||||
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
|
||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
};
|
||||
@ -74,7 +91,7 @@ export const constants = {
|
||||
ETH_GAS_STATION_API_URL,
|
||||
PROTOCOL_FEE_MULTIPLIER,
|
||||
NULL_BYTES,
|
||||
ZERO_AMOUNT: new BigNumber(0),
|
||||
ZERO_AMOUNT,
|
||||
NULL_ADDRESS,
|
||||
MAINNET_CHAIN_ID,
|
||||
DEFAULT_ORDER_PRUNER_OPTS,
|
||||
@ -85,6 +102,8 @@ export const constants = {
|
||||
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
||||
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
|
||||
DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS,
|
||||
DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
|
||||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_RFQT_REQUEST_OPTS,
|
||||
NULL_ERC20_ASSET_DATA,
|
||||
|
@ -40,6 +40,7 @@ export { InsufficientAssetLiquidityError } from './errors';
|
||||
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
|
||||
export { SwapQuoter } from './swap_quoter';
|
||||
export {
|
||||
AffiliateFee,
|
||||
CalldataInfo,
|
||||
ExchangeProxyContractOpts,
|
||||
ExtensionContractType,
|
||||
|
@ -2,6 +2,7 @@ import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { ITransformERC20Contract } from '@0x/contract-wrappers';
|
||||
import {
|
||||
assetDataUtils,
|
||||
encodeAffiliateFeeTransformerData,
|
||||
encodeFillQuoteTransformerData,
|
||||
encodePayTakerTransformerData,
|
||||
encodeWethTransformerData,
|
||||
@ -18,6 +19,7 @@ import * as _ from 'lodash';
|
||||
import { constants } from '../constants';
|
||||
import {
|
||||
CalldataInfo,
|
||||
ExchangeProxyContractOpts,
|
||||
MarketBuySwapQuote,
|
||||
MarketOperation,
|
||||
MarketSellSwapQuote,
|
||||
@ -41,6 +43,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
wethTransformer: number;
|
||||
payTakerTransformer: number;
|
||||
fillQuoteTransformer: number;
|
||||
affiliateFeeTransformer: number;
|
||||
};
|
||||
|
||||
private readonly _transformFeature: ITransformERC20Contract;
|
||||
@ -70,6 +73,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
contractAddresses.transformers.fillQuoteTransformer,
|
||||
contractAddresses.exchangeProxyTransformerDeployer,
|
||||
),
|
||||
affiliateFeeTransformer: findTransformerNonce(
|
||||
contractAddresses.transformers.affiliateFeeTransformer,
|
||||
contractAddresses.exchangeProxyTransformerDeployer,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -78,14 +85,11 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||
): Promise<CalldataInfo> {
|
||||
assert.isValidSwapQuote('quote', quote);
|
||||
const { isFromETH, isToETH } = {
|
||||
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||
extensionContractOpts: {
|
||||
isFromETH: false,
|
||||
isToETH: false,
|
||||
},
|
||||
...opts,
|
||||
}.extensionContractOpts;
|
||||
// tslint:disable-next-line:no-object-literal-type-assertion
|
||||
const { affiliateFee, isFromETH, isToETH } = {
|
||||
...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
|
||||
...opts.extensionContractOpts,
|
||||
} as ExchangeProxyContractOpts;
|
||||
|
||||
const sellToken = getTokenFromAssetData(quote.takerAssetData);
|
||||
const buyToken = getTokenFromAssetData(quote.makerAssetData);
|
||||
@ -129,6 +133,28 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
});
|
||||
}
|
||||
|
||||
// This transformer pays affiliate fees.
|
||||
const { buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee;
|
||||
|
||||
if (buyTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) {
|
||||
transforms.push({
|
||||
deploymentNonce: this.transformerNonces.affiliateFeeTransformer,
|
||||
data: encodeAffiliateFeeTransformerData({
|
||||
fees: [
|
||||
{
|
||||
token: isToETH ? ETH_TOKEN_ADDRESS : buyToken,
|
||||
amount: buyTokenFeeAmount,
|
||||
recipient: feeRecipient,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (sellTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) {
|
||||
throw new Error('Affiliate fees denominated in sell token are not yet supported');
|
||||
}
|
||||
|
||||
// The final transformer will send all funds to the taker.
|
||||
transforms.push({
|
||||
deploymentNonce: this.transformerNonces.payTakerTransformer,
|
||||
@ -138,12 +164,13 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
}),
|
||||
});
|
||||
|
||||
const minBuyAmount = BigNumber.max(0, quote.worstCaseQuoteInfo.makerAssetAmount.minus(buyTokenFeeAmount));
|
||||
const calldataHexString = this._transformFeature
|
||||
.transformERC20(
|
||||
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
|
||||
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
|
||||
sellAmount,
|
||||
quote.worstCaseQuoteInfo.makerAssetAmount,
|
||||
minBuyAmount,
|
||||
transforms,
|
||||
)
|
||||
.getABIEncodedTransactionData();
|
||||
|
@ -124,13 +124,21 @@ export interface ForwarderExtensionContractOpts {
|
||||
feeRecipient: string;
|
||||
}
|
||||
|
||||
export interface AffiliateFee {
|
||||
recipient: string;
|
||||
buyTokenFeeAmount: BigNumber;
|
||||
sellTokenFeeAmount: BigNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isFromETH Whether the input token is ETH.
|
||||
* @param isToETH Whether the output token is ETH.
|
||||
* @param affiliateFee Fee denominated in taker or maker asset to send to specified recipient.
|
||||
*/
|
||||
export interface ExchangeProxyContractOpts {
|
||||
isFromETH: boolean;
|
||||
isToETH: boolean;
|
||||
affiliateFee: AffiliateFee;
|
||||
}
|
||||
|
||||
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
||||
|
@ -16,13 +16,7 @@ import {
|
||||
|
||||
import { MarketOperationUtils } from './market_operation_utils';
|
||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||
import {
|
||||
FeeSchedule,
|
||||
FillData,
|
||||
GetMarketOrdersOpts,
|
||||
OptimizedMarketOrder,
|
||||
OptimizedOrdersAndQuoteReport,
|
||||
} from './market_operation_utils/types';
|
||||
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||
import { isSupportedAssetDataInOrders } from './utils';
|
||||
|
||||
import { QuoteReport } from './quote_report_generator';
|
||||
|
@ -2,6 +2,7 @@ import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
|
||||
import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
|
||||
import {
|
||||
assetDataUtils,
|
||||
decodeAffiliateFeeTransformerData,
|
||||
decodeFillQuoteTransformerData,
|
||||
decodePayTakerTransformerData,
|
||||
decodeWethTransformerData,
|
||||
@ -28,7 +29,7 @@ chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
const { NULL_ADDRESS } = constants;
|
||||
const { MAX_UINT256 } = contractConstants;
|
||||
const { MAX_UINT256, ZERO_AMOUNT } = contractConstants;
|
||||
|
||||
// tslint:disable: custom-no-magic-numbers
|
||||
|
||||
@ -265,5 +266,37 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
|
||||
expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
|
||||
});
|
||||
it('Appends an affiliate fee transformer after the fill if a buy token affiliate fee is provided', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const affiliateFee = {
|
||||
recipient: randomAddress(),
|
||||
buyTokenFeeAmount: getRandomAmount(),
|
||||
sellTokenFeeAmount: ZERO_AMOUNT,
|
||||
};
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
|
||||
extensionContractOpts: { affiliateFee },
|
||||
});
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.transformations[1].deploymentNonce.toNumber()).to.eq(
|
||||
consumer.transformerNonces.affiliateFeeTransformer,
|
||||
);
|
||||
const affiliateFeeTransformerData = decodeAffiliateFeeTransformerData(callArgs.transformations[1].data);
|
||||
expect(affiliateFeeTransformerData.fees).to.deep.equal([
|
||||
{ token: MAKER_TOKEN, amount: affiliateFee.buyTokenFeeAmount, recipient: affiliateFee.recipient },
|
||||
]);
|
||||
});
|
||||
it('Throws if a sell token affiliate fee is provided', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const affiliateFee = {
|
||||
recipient: randomAddress(),
|
||||
buyTokenFeeAmount: ZERO_AMOUNT,
|
||||
sellTokenFeeAmount: getRandomAmount(),
|
||||
};
|
||||
expect(
|
||||
consumer.getCalldataOrThrowAsync(quote, {
|
||||
extensionContractOpts: { affiliateFee },
|
||||
}),
|
||||
).to.eventually.be.rejectedWith('Affiliate fees denominated in sell token are not yet supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -183,5 +183,5 @@ export function encodeAffiliateFeeTransformerData(data: AffiliateFeeTransformerD
|
||||
* ABI-decode a `AffiliateFeeTransformer.TransformData` type.
|
||||
*/
|
||||
export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFeeTransformerData {
|
||||
return affiliateFeeTransformerDataEncoder.decode(encoded).data;
|
||||
return affiliateFeeTransformerDataEncoder.decode(encoded);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user