@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 { BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ExchangeProxyContractOpts,
|
||||||
ExtensionContractType,
|
ExtensionContractType,
|
||||||
ForwarderExtensionContractOpts,
|
ForwarderExtensionContractOpts,
|
||||||
OrderPrunerOpts,
|
OrderPrunerOpts,
|
||||||
@ -20,6 +21,7 @@ const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|||||||
const MAINNET_CHAIN_ID = 1;
|
const MAINNET_CHAIN_ID = 1;
|
||||||
const ONE_SECOND_MS = 1000;
|
const ONE_SECOND_MS = 1000;
|
||||||
const DEFAULT_PER_PAGE = 1000;
|
const DEFAULT_PER_PAGE = 1000;
|
||||||
|
const ZERO_AMOUNT = new BigNumber(0);
|
||||||
|
|
||||||
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
|
||||||
expiryBufferMs: 120000, // 2 minutes
|
expiryBufferMs: 120000, // 2 minutes
|
||||||
@ -60,8 +62,23 @@ const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
|
|||||||
extensionContractOpts: DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS,
|
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_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 = {
|
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
|
||||||
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
...DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||||
};
|
};
|
||||||
@ -74,7 +91,7 @@ export const constants = {
|
|||||||
ETH_GAS_STATION_API_URL,
|
ETH_GAS_STATION_API_URL,
|
||||||
PROTOCOL_FEE_MULTIPLIER,
|
PROTOCOL_FEE_MULTIPLIER,
|
||||||
NULL_BYTES,
|
NULL_BYTES,
|
||||||
ZERO_AMOUNT: new BigNumber(0),
|
ZERO_AMOUNT,
|
||||||
NULL_ADDRESS,
|
NULL_ADDRESS,
|
||||||
MAINNET_CHAIN_ID,
|
MAINNET_CHAIN_ID,
|
||||||
DEFAULT_ORDER_PRUNER_OPTS,
|
DEFAULT_ORDER_PRUNER_OPTS,
|
||||||
@ -85,6 +102,8 @@ export const constants = {
|
|||||||
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||||
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
||||||
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
|
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
|
||||||
|
DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS,
|
||||||
|
DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
|
||||||
DEFAULT_PER_PAGE,
|
DEFAULT_PER_PAGE,
|
||||||
DEFAULT_RFQT_REQUEST_OPTS,
|
DEFAULT_RFQT_REQUEST_OPTS,
|
||||||
NULL_ERC20_ASSET_DATA,
|
NULL_ERC20_ASSET_DATA,
|
||||||
|
@ -40,6 +40,7 @@ export { InsufficientAssetLiquidityError } from './errors';
|
|||||||
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
|
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
|
||||||
export { SwapQuoter } from './swap_quoter';
|
export { SwapQuoter } from './swap_quoter';
|
||||||
export {
|
export {
|
||||||
|
AffiliateFee,
|
||||||
CalldataInfo,
|
CalldataInfo,
|
||||||
ExchangeProxyContractOpts,
|
ExchangeProxyContractOpts,
|
||||||
ExtensionContractType,
|
ExtensionContractType,
|
||||||
|
@ -2,6 +2,7 @@ import { ContractAddresses } from '@0x/contract-addresses';
|
|||||||
import { ITransformERC20Contract } from '@0x/contract-wrappers';
|
import { ITransformERC20Contract } from '@0x/contract-wrappers';
|
||||||
import {
|
import {
|
||||||
assetDataUtils,
|
assetDataUtils,
|
||||||
|
encodeAffiliateFeeTransformerData,
|
||||||
encodeFillQuoteTransformerData,
|
encodeFillQuoteTransformerData,
|
||||||
encodePayTakerTransformerData,
|
encodePayTakerTransformerData,
|
||||||
encodeWethTransformerData,
|
encodeWethTransformerData,
|
||||||
@ -18,6 +19,7 @@ import * as _ from 'lodash';
|
|||||||
import { constants } from '../constants';
|
import { constants } from '../constants';
|
||||||
import {
|
import {
|
||||||
CalldataInfo,
|
CalldataInfo,
|
||||||
|
ExchangeProxyContractOpts,
|
||||||
MarketBuySwapQuote,
|
MarketBuySwapQuote,
|
||||||
MarketOperation,
|
MarketOperation,
|
||||||
MarketSellSwapQuote,
|
MarketSellSwapQuote,
|
||||||
@ -41,6 +43,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
wethTransformer: number;
|
wethTransformer: number;
|
||||||
payTakerTransformer: number;
|
payTakerTransformer: number;
|
||||||
fillQuoteTransformer: number;
|
fillQuoteTransformer: number;
|
||||||
|
affiliateFeeTransformer: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _transformFeature: ITransformERC20Contract;
|
private readonly _transformFeature: ITransformERC20Contract;
|
||||||
@ -70,6 +73,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
contractAddresses.transformers.fillQuoteTransformer,
|
contractAddresses.transformers.fillQuoteTransformer,
|
||||||
contractAddresses.exchangeProxyTransformerDeployer,
|
contractAddresses.exchangeProxyTransformerDeployer,
|
||||||
),
|
),
|
||||||
|
affiliateFeeTransformer: findTransformerNonce(
|
||||||
|
contractAddresses.transformers.affiliateFeeTransformer,
|
||||||
|
contractAddresses.exchangeProxyTransformerDeployer,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,14 +85,11 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||||
): Promise<CalldataInfo> {
|
): Promise<CalldataInfo> {
|
||||||
assert.isValidSwapQuote('quote', quote);
|
assert.isValidSwapQuote('quote', quote);
|
||||||
const { isFromETH, isToETH } = {
|
// tslint:disable-next-line:no-object-literal-type-assertion
|
||||||
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
const { affiliateFee, isFromETH, isToETH } = {
|
||||||
extensionContractOpts: {
|
...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
|
||||||
isFromETH: false,
|
...opts.extensionContractOpts,
|
||||||
isToETH: false,
|
} as ExchangeProxyContractOpts;
|
||||||
},
|
|
||||||
...opts,
|
|
||||||
}.extensionContractOpts;
|
|
||||||
|
|
||||||
const sellToken = getTokenFromAssetData(quote.takerAssetData);
|
const sellToken = getTokenFromAssetData(quote.takerAssetData);
|
||||||
const buyToken = getTokenFromAssetData(quote.makerAssetData);
|
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.
|
// The final transformer will send all funds to the taker.
|
||||||
transforms.push({
|
transforms.push({
|
||||||
deploymentNonce: this.transformerNonces.payTakerTransformer,
|
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
|
const calldataHexString = this._transformFeature
|
||||||
.transformERC20(
|
.transformERC20(
|
||||||
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
|
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
|
||||||
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
|
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
|
||||||
sellAmount,
|
sellAmount,
|
||||||
quote.worstCaseQuoteInfo.makerAssetAmount,
|
minBuyAmount,
|
||||||
transforms,
|
transforms,
|
||||||
)
|
)
|
||||||
.getABIEncodedTransactionData();
|
.getABIEncodedTransactionData();
|
||||||
|
@ -124,13 +124,21 @@ export interface ForwarderExtensionContractOpts {
|
|||||||
feeRecipient: string;
|
feeRecipient: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AffiliateFee {
|
||||||
|
recipient: string;
|
||||||
|
buyTokenFeeAmount: BigNumber;
|
||||||
|
sellTokenFeeAmount: BigNumber;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isFromETH Whether the input token is ETH.
|
* @param isFromETH Whether the input token is ETH.
|
||||||
* @param isToETH Whether the output 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 {
|
export interface ExchangeProxyContractOpts {
|
||||||
isFromETH: boolean;
|
isFromETH: boolean;
|
||||||
isToETH: boolean;
|
isToETH: boolean;
|
||||||
|
affiliateFee: AffiliateFee;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
||||||
|
@ -16,13 +16,7 @@ import {
|
|||||||
|
|
||||||
import { MarketOperationUtils } from './market_operation_utils';
|
import { MarketOperationUtils } from './market_operation_utils';
|
||||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||||
import {
|
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||||
FeeSchedule,
|
|
||||||
FillData,
|
|
||||||
GetMarketOrdersOpts,
|
|
||||||
OptimizedMarketOrder,
|
|
||||||
OptimizedOrdersAndQuoteReport,
|
|
||||||
} from './market_operation_utils/types';
|
|
||||||
import { isSupportedAssetDataInOrders } from './utils';
|
import { isSupportedAssetDataInOrders } from './utils';
|
||||||
|
|
||||||
import { QuoteReport } from './quote_report_generator';
|
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 { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
|
||||||
import {
|
import {
|
||||||
assetDataUtils,
|
assetDataUtils,
|
||||||
|
decodeAffiliateFeeTransformerData,
|
||||||
decodeFillQuoteTransformerData,
|
decodeFillQuoteTransformerData,
|
||||||
decodePayTakerTransformerData,
|
decodePayTakerTransformerData,
|
||||||
decodeWethTransformerData,
|
decodeWethTransformerData,
|
||||||
@ -28,7 +29,7 @@ chaiSetup.configure();
|
|||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
|
|
||||||
const { NULL_ADDRESS } = constants;
|
const { NULL_ADDRESS } = constants;
|
||||||
const { MAX_UINT256 } = contractConstants;
|
const { MAX_UINT256, ZERO_AMOUNT } = contractConstants;
|
||||||
|
|
||||||
// tslint:disable: custom-no-magic-numbers
|
// tslint:disable: custom-no-magic-numbers
|
||||||
|
|
||||||
@ -265,5 +266,37 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
|||||||
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
|
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
|
||||||
expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
|
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.
|
* ABI-decode a `AffiliateFeeTransformer.TransformData` type.
|
||||||
*/
|
*/
|
||||||
export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFeeTransformerData {
|
export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFeeTransformerData {
|
||||||
return affiliateFeeTransformerDataEncoder.decode(encoded).data;
|
return affiliateFeeTransformerDataEncoder.decode(encoded);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user