diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 37402629cf..bdd0538064 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Update `CurveBridge` to support more varied curves", "pr": 2633 + }, + { + "note": "Export DexForwarderBridgeContract", + "pr": 2656 } ] }, diff --git a/contracts/asset-proxy/src/index.ts b/contracts/asset-proxy/src/index.ts index b95851265b..7e31c510b3 100644 --- a/contracts/asset-proxy/src/index.ts +++ b/contracts/asset-proxy/src/index.ts @@ -18,6 +18,7 @@ export { TestDydxBridgeContract, TestStaticCallTargetContract, UniswapBridgeContract, + DexForwarderBridgeContract, } from './wrappers'; export { ERC20Wrapper } from './erc20_wrapper'; diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 51e56d1a17..a64aa17943 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.5.2", + "changes": [ + { + "note": "Add Ropsten and Rinkeby addresses to `DeploymentConstants`", + "pr": 2656 + } + ] + }, { "version": "4.5.1", "changes": [ diff --git a/contracts/utils/contracts/src/DeploymentConstants.sol b/contracts/utils/contracts/src/DeploymentConstants.sol index 7950ccc0e5..0f387fbf90 100644 --- a/contracts/utils/contracts/src/DeploymentConstants.sol +++ b/contracts/utils/contracts/src/DeploymentConstants.sol @@ -51,33 +51,89 @@ contract DeploymentConstants { /// @dev Mainnet address of the GST Collector address constant private GST_COLLECTOR_ADDRESS = 0x000000D3b08566BE75A6DB803C03C85C0c1c5B96; - // Kovan addresses ///////////////////////////////////////////////////////// - // /// @dev Kovan address of the WETH contract. - // address constant private WETH_ADDRESS = 0xd0A1E359811322d97991E03f863a0C30C2cF029C; - // /// @dev Kovan address of the KyberNetworkProxy contract. - // address constant private KYBER_NETWORK_PROXY_ADDRESS = 0x692f391bCc85cefCe8C237C01e1f636BbD70EA4D; - // /// @dev Kovan address of the `UniswapExchangeFactory` contract. - // address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30; - // /// @dev Kovan address of the `UniswapV2Router01` contract. - // address constant private UNISWAP_V2_ROUTER_01_ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; - // /// @dev Kovan address of the Eth2Dai `MatchingMarket` contract. - // address constant private ETH2DAI_ADDRESS = 0xe325acB9765b02b8b418199bf9650972299235F4; - // /// @dev Kovan address of the `ERC20BridgeProxy` contract - // address constant private ERC20_BRIDGE_PROXY_ADDRESS = 0xFb2DD2A1366dE37f7241C83d47DA58fd503E2C64; - // /// @dev Kovan address of the `Chai` contract - // address constant private CHAI_ADDRESS = address(0); - // /// @dev Kovan address of the `Dai` (multi-collateral) contract - // address constant private DAI_ADDRESS = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa; - // /// @dev Kovan address of the 0x DevUtils contract. - // address constant private DEV_UTILS_ADDRESS = 0x9402639A828BdF4E9e4103ac3B69E1a6E522eB59; - // /// @dev Kyber ETH pseudo-address. - // address constant internal KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - // /// @dev Kovan address of the dYdX contract. - // address constant private DYDX_ADDRESS = address(0); - // /// @dev Kovan address of the GST2 contract - // address constant private GST_ADDRESS = address(0); - // /// @dev Kovan address of the GST Collector - // address constant private GST_COLLECTOR_ADDRESS = address(0); + /* // Ropsten addresses /////////////////////////////////////////////////////// + /// @dev Mainnet address of the WETH contract. + address constant private WETH_ADDRESS = 0xc778417E063141139Fce010982780140Aa0cD5Ab; + /// @dev Mainnet address of the KyberNetworkProxy contract. + address constant private KYBER_NETWORK_PROXY_ADDRESS = address(0); + /// @dev Mainnet address of the `UniswapExchangeFactory` contract. + address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = address(0); + /// @dev Mainnet address of the `UniswapV2Router01` contract. + address constant private UNISWAP_V2_ROUTER_01_ADDRESS = address(0); + /// @dev Mainnet address of the Eth2Dai `MatchingMarket` contract. + address constant private ETH2DAI_ADDRESS = address(0); + /// @dev Mainnet address of the `ERC20BridgeProxy` contract + address constant private ERC20_BRIDGE_PROXY_ADDRESS = 0xb344afeD348de15eb4a9e180205A2B0739628339; + ///@dev Mainnet address of the `Dai` (multi-collateral) contract + address constant private DAI_ADDRESS = address(0); + /// @dev Mainnet address of the `Chai` contract + address constant private CHAI_ADDRESS = address(0); + /// @dev Mainnet address of the 0x DevUtils contract. + address constant private DEV_UTILS_ADDRESS = 0xC812AF3f3fBC62F76ea4262576EC0f49dB8B7f1c; + /// @dev Kyber ETH pseudo-address. + address constant internal KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev Mainnet address of the dYdX contract. + address constant private DYDX_ADDRESS = address(0); + /// @dev Mainnet address of the GST2 contract + address constant private GST_ADDRESS = address(0); + /// @dev Mainnet address of the GST Collector + address constant private GST_COLLECTOR_ADDRESS = address(0); */ + + /* // Rinkeby addresses /////////////////////////////////////////////////////// + /// @dev Mainnet address of the WETH contract. + address constant private WETH_ADDRESS = 0xc778417E063141139Fce010982780140Aa0cD5Ab; + /// @dev Mainnet address of the KyberNetworkProxy contract. + address constant private KYBER_NETWORK_PROXY_ADDRESS = address(0); + /// @dev Mainnet address of the `UniswapExchangeFactory` contract. + address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = address(0); + /// @dev Mainnet address of the `UniswapV2Router01` contract. + address constant private UNISWAP_V2_ROUTER_01_ADDRESS = address(0); + /// @dev Mainnet address of the Eth2Dai `MatchingMarket` contract. + address constant private ETH2DAI_ADDRESS = address(0); + /// @dev Mainnet address of the `ERC20BridgeProxy` contract + address constant private ERC20_BRIDGE_PROXY_ADDRESS = 0xA2AA4bEFED748Fba27a3bE7Dfd2C4b2c6DB1F49B; + ///@dev Mainnet address of the `Dai` (multi-collateral) contract + address constant private DAI_ADDRESS = address(0); + /// @dev Mainnet address of the `Chai` contract + address constant private CHAI_ADDRESS = address(0); + /// @dev Mainnet address of the 0x DevUtils contract. + address constant private DEV_UTILS_ADDRESS = 0x46B5BC959e8A754c0256FFF73bF34A52Ad5CdfA9; + /// @dev Kyber ETH pseudo-address. + address constant internal KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev Mainnet address of the dYdX contract. + address constant private DYDX_ADDRESS = address(0); + /// @dev Mainnet address of the GST2 contract + address constant private GST_ADDRESS = address(0); + /// @dev Mainnet address of the GST Collector + address constant private GST_COLLECTOR_ADDRESS = address(0); */ + + /* // Kovan addresses ///////////////////////////////////////////////////////// + /// @dev Kovan address of the WETH contract. + address constant private WETH_ADDRESS = 0xd0A1E359811322d97991E03f863a0C30C2cF029C; + /// @dev Kovan address of the KyberNetworkProxy contract. + address constant private KYBER_NETWORK_PROXY_ADDRESS = 0x692f391bCc85cefCe8C237C01e1f636BbD70EA4D; + /// @dev Kovan address of the `UniswapExchangeFactory` contract. + address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30; + /// @dev Kovan address of the `UniswapV2Router01` contract. + address constant private UNISWAP_V2_ROUTER_01_ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; + /// @dev Kovan address of the Eth2Dai `MatchingMarket` contract. + address constant private ETH2DAI_ADDRESS = 0xe325acB9765b02b8b418199bf9650972299235F4; + /// @dev Kovan address of the `ERC20BridgeProxy` contract + address constant private ERC20_BRIDGE_PROXY_ADDRESS = 0x3577552C1Fb7A44aD76BeEB7aB53251668A21F8D; + /// @dev Kovan address of the `Chai` contract + address constant private CHAI_ADDRESS = address(0); + /// @dev Kovan address of the `Dai` (multi-collateral) contract + address constant private DAI_ADDRESS = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa; + /// @dev Kovan address of the 0x DevUtils contract. + address constant private DEV_UTILS_ADDRESS = 0x9402639A828BdF4E9e4103ac3B69E1a6E522eB59; + /// @dev Kyber ETH pseudo-address. + address constant internal KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev Kovan address of the dYdX contract. + address constant private DYDX_ADDRESS = address(0); + /// @dev Kovan address of the GST2 contract + address constant private GST_ADDRESS = address(0); + /// @dev Kovan address of the GST Collector + address constant private GST_COLLECTOR_ADDRESS = address(0); */ /// @dev Overridable way to get the `KyberNetworkProxy` address. /// @return kyberAddress The `IKyberNetworkProxy` address. diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index a603c24b93..985e62fc27 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -2,6 +2,10 @@ { "version": "4.7.0", "changes": [ + { + "note": "Return quoteReport from SwapQuoter functions", + "pr": 2627 + }, { "note": "Allow an empty override for sampler overrides", "pr": 2637 @@ -13,6 +17,34 @@ { "note": "Support more varied curves", "pr": 2633 + }, + { + "note": "Make path optimization go faster", + "pr": 2640 + }, + { + "note": "Adds `getBidAskLiquidityForMakerTakerAssetPairAsync` to return more detailed sample information", + "pr": 2641 + }, + { + "note": "Fix regression where a split on the same source was collapsed into a single fill", + "pr": 2654 + }, + { + "note": "Add support for buy token affiliate fees", + "pr": 2658 + }, + { + "note": "Fix optimization of buy paths", + "pr": 2655 + }, + { + "note": "Fix depth buy scale", + "pr": 2659 + }, + { + "note": "Adjust fill by ethToInputRate when ethToOutputRate is 0", + "pr": 2660 } ] }, diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index c7dc690e32..d24ff0154a 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -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, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 7fd68e151e..1f75620bd2 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -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, @@ -66,23 +67,37 @@ export { SwapQuoteRequestOpts, SwapQuoterError, SwapQuoterOpts, + SwapQuoterRfqtOpts, } from './types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { BalancerFillData, CollapsedFill, CurveFillData, + CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, FeeSchedule, FillData, GetMarketOrdersRfqtOpts, + LiquidityProviderFillData, + MarketDepth, + MarketDepthSide, + MultiBridgeFillData, NativeCollapsedFill, NativeFillData, OptimizedMarketOrder, UniswapV2FillData, - CurveFunctionSelectors, } from './utils/market_operation_utils/types'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; +export { + BridgeReportSource, + NativeOrderbookReportSource, + NativeRFQTReportSource, + QuoteReport, + QuoteReportSource, +} from './utils/quote_report_generator'; export { QuoteRequestor } from './utils/quote_requestor'; export { rfqtMocker } from './utils/rfqt_mocker'; +import { ERC20BridgeSource } from './utils/market_operation_utils/types'; +export type Native = ERC20BridgeSource.Native; diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index 89dc4a95d6..5fb2c44f63 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -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 = {}, ): Promise { 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(); diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 5817743e40..08d9ab83f3 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -20,13 +20,19 @@ import { SwapQuote, SwapQuoteRequestOpts, SwapQuoterOpts, + SwapQuoterRfqtOpts, } from './types'; import { assert } from './utils/assert'; import { calculateLiquidity } from './utils/calculate_liquidity'; import { MarketOperationUtils } from './utils/market_operation_utils'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { DexOrderSampler } from './utils/market_operation_utils/sampler'; -import { ERC20BridgeSource } from './utils/market_operation_utils/types'; +import { + ERC20BridgeSource, + MarketDepth, + MarketDepthSide, + MarketSideLiquidity, +} from './utils/market_operation_utils/types'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; @@ -46,8 +52,7 @@ export class SwapQuoter { private readonly _devUtilsContract: DevUtilsContract; private readonly _marketOperationUtils: MarketOperationUtils; private readonly _orderStateUtils: OrderStateUtils; - private readonly _quoteRequestor: QuoteRequestor; - private readonly _rfqtTakerApiKeyWhitelist: string[]; + private readonly _rfqtOptions?: SwapQuoterRfqtOpts; /** * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. @@ -168,7 +173,8 @@ export class SwapQuoter { this.orderbook = orderbook; this.expiryBufferMs = expiryBufferMs; this.permittedOrderFeeTypes = permittedOrderFeeTypes; - this._rfqtTakerApiKeyWhitelist = rfqt ? rfqt.takerApiKeyWhitelist || [] : []; + + this._rfqtOptions = rfqt; this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = ProtocolFeeUtils.getInstance( @@ -176,12 +182,6 @@ export class SwapQuoter { options.ethGasStationUrl, ); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); - this._quoteRequestor = new QuoteRequestor( - rfqt ? rfqt.makerAssetOfferings || {} : {}, - rfqt ? rfqt.warningLogger : undefined, - rfqt ? rfqt.infoLogger : undefined, - expiryBufferMs, - ); // Allow the sampler bytecode to be overwritten using geths override functionality const samplerBytecode = _.get(ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object'); const defaultCodeOverrides = samplerBytecode @@ -398,6 +398,98 @@ export class SwapQuoter { return calculateLiquidity(ordersWithFillableAmounts); } + /** + * Returns the bids and asks liquidity for the entire market. + * For certain sources (like AMM's) it is recommended to provide a practical maximum takerAssetAmount. + * @param makerTokenAddress The address of the maker asset + * @param takerTokenAddress The address of the taker asset + * @param takerAssetAmount The amount to sell and buy for the bids and asks. + * + * @return An object that conforms to MarketDepth that contains all of the samples and liquidity + * information for the source. + */ + public async getBidAskLiquidityForMakerTakerAssetPairAsync( + makerTokenAddress: string, + takerTokenAddress: string, + takerAssetAmount: BigNumber, + options: Partial = {}, + ): Promise { + assert.isString('makerTokenAddress', makerTokenAddress); + assert.isString('takerTokenAddress', takerTokenAddress); + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); + let [sellOrders, buyOrders] = + options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native) + ? Promise.resolve([[], []]) + : await Promise.all([ + this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), + this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), + ]); + if (!sellOrders || sellOrders.length === 0) { + sellOrders = [ + { + metaData: {}, + order: createDummyOrderForSampler( + makerAssetData, + takerAssetData, + this._contractAddresses.uniswapBridge, + ), + }, + ]; + } + if (!buyOrders || buyOrders.length === 0) { + buyOrders = [ + { + metaData: {}, + order: createDummyOrderForSampler( + takerAssetData, + makerAssetData, + this._contractAddresses.uniswapBridge, + ), + }, + ]; + } + const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => { + const { dexQuotes, nativeOrders, orderFillableAmounts, side } = marketSideLiquidity; + return [ + ...dexQuotes, + nativeOrders.map((o, i) => { + // When sell order fillable amount is taker + // When buy order fillable amount is maker + const scaleFactor = orderFillableAmounts[i].div( + side === MarketOperation.Sell ? o.takerAssetAmount : o.makerAssetAmount, + ); + return { + input: (side === MarketOperation.Sell ? o.takerAssetAmount : o.makerAssetAmount) + .times(scaleFactor) + .integerValue(), + output: (side === MarketOperation.Sell ? o.makerAssetAmount : o.takerAssetAmount) + .times(scaleFactor) + .integerValue(), + fillData: o, + source: ERC20BridgeSource.Native, + }; + }), + ]; + }; + const [bids, asks] = await Promise.all([ + this._marketOperationUtils.getMarketBuyLiquidityAsync( + (buyOrders || []).map(o => o.order), + takerAssetAmount, + options, + ), + this._marketOperationUtils.getMarketSellLiquidityAsync( + (sellOrders || []).map(o => o.order), + takerAssetAmount, + options, + ), + ]); + return { + bids: getMarketDepthSide(bids), + asks: getMarketDepthSide(asks), + }; + } + /** * Get the asset data of all assets that can be used to purchase makerAssetData in the order provider passed in at init. * @@ -569,24 +661,34 @@ export class SwapQuoter { // get batches of orders from different sources, awaiting sources in parallel const orderBatchPromises: Array> = []; - orderBatchPromises.push( - // Don't fetch Open Orderbook orders from the DB if Native has been excluded, or if `nativeExclusivelyRFQT` has been set. + + const skipOpenOrderbook = opts.excludedSources.includes(ERC20BridgeSource.Native) || - (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true) - ? Promise.resolve([]) - : this._getSignedOrdersAsync(makerAssetData, takerAssetData), + (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true); + if (!skipOpenOrderbook) { + orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book + } + + const rfqtOptions = this._rfqtOptions; + const quoteRequestor = new QuoteRequestor( + rfqtOptions ? rfqtOptions.makerAssetOfferings || {} : {}, + rfqtOptions ? rfqtOptions.warningLogger : undefined, + rfqtOptions ? rfqtOptions.infoLogger : undefined, + this.expiryBufferMs, ); + if ( opts.rfqt && // This is an RFQT-enabled API request opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote - !opts.excludedSources.includes(ERC20BridgeSource.Native) && // Native liquidity is not excluded - this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) // A valid API key was provided + opts.rfqt.apiKey && + this._isApiKeyWhitelisted(opts.rfqt.apiKey) && // A valid API key was provided + !opts.excludedSources.includes(ERC20BridgeSource.Native) // Native liquidity is not excluded ) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { throw new Error('RFQ-T requests must specify a taker address'); } orderBatchPromises.push( - this._quoteRequestor + quoteRequestor .requestRfqtFirmQuotesAsync( makerAssetData, takerAssetData, @@ -600,7 +702,7 @@ export class SwapQuoter { const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises); - const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch)); + const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch), []); const orders = sortingUtils.sortOrders(unsortedOrders); @@ -615,8 +717,8 @@ export class SwapQuoter { const calcOpts: CalculateSwapQuoteOpts = opts; - if (calcOpts.rfqt !== undefined && this._shouldEnableIndicativeRfqt(calcOpts.rfqt, marketOperation)) { - calcOpts.rfqt.quoteRequestor = this._quoteRequestor; + if (calcOpts.rfqt !== undefined) { + calcOpts.rfqt.quoteRequestor = quoteRequestor; } if (marketOperation === MarketOperation.Buy) { @@ -637,13 +739,9 @@ export class SwapQuoter { return swapQuote; } - private _shouldEnableIndicativeRfqt(opts: CalculateSwapQuoteOpts['rfqt'], op: MarketOperation): boolean { - return ( - opts !== undefined && - opts.isIndicative !== undefined && - opts.isIndicative && - this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) - ); + private _isApiKeyWhitelisted(apiKey: string): boolean { + const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : []; + return whitelistedApiKeys.includes(apiKey); } } // tslint:disable-next-line: max-file-line-count diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 1bf1d1e69c..bd193ef406 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types'; +import { QuoteReport } from './utils/quote_report_generator'; import { LogFunction } from './utils/quote_requestor'; /** @@ -123,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; @@ -155,6 +164,7 @@ export interface SwapQuoteBase { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown; + quoteReport?: QuoteReport; } /** @@ -236,6 +246,13 @@ export interface RfqtMakerAssetOfferings { export { LogFunction } from './utils/quote_requestor'; +export interface SwapQuoterRfqtOpts { + takerApiKeyWhitelist: string[]; + makerAssetOfferings: RfqtMakerAssetOfferings; + warningLogger?: LogFunction; + infoLogger?: LogFunction; +} + /** * chainId: The ethereum chain id. Defaults to 1 (mainnet). * orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). @@ -252,12 +269,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { liquidityProviderRegistryAddress?: string; multiBridgeAddress?: string; ethGasStationUrl?: string; - rfqt?: { - takerApiKeyWhitelist: string[]; - makerAssetOfferings: RfqtMakerAssetOfferings; - warningLogger?: LogFunction; - infoLogger?: LogFunction; - }; + rfqt?: SwapQuoterRfqtOpts; samplerOverrides?: SamplerOverrides; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 6908eca747..f06445c091 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -1,4 +1,4 @@ -import { BigNumber } from '@0x/utils'; +import { BigNumber, hexUtils } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; @@ -17,6 +17,7 @@ export function createFillPaths(opts: { dexQuotes?: DexSample[][]; targetInput?: BigNumber; ethToOutputRate?: BigNumber; + ethToInputRate?: BigNumber; excludedSources?: ERC20BridgeSource[]; feeSchedule?: FeeSchedule; }): Fill[][] { @@ -26,8 +27,9 @@ export function createFillPaths(opts: { const orders = opts.orders || []; const dexQuotes = opts.dexQuotes || []; const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; + const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; // Create native fill paths. - const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, feeSchedule); + const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, ethToInputRate, feeSchedule); // Create DEX fill paths. const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule); return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources); @@ -54,19 +56,21 @@ function nativeOrdersToPath( orders: SignedOrderWithFillableAmounts[], targetInput: BigNumber = POSITIVE_INF, ethToOutputRate: BigNumber, + ethToInputRate: BigNumber, fees: FeeSchedule, ): Fill[] { + const sourcePathId = hexUtils.random(); // Create a single path from all orders. - let path: Fill[] = []; + let path: Array = []; for (const order of orders) { const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const output = side === MarketOperation.Sell ? makerAmount : takerAmount; - const penalty = ethToOutputRate.times( - fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(), - ); - const rate = makerAmount.div(takerAmount); + const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(); + const outputPenalty = !ethToOutputRate.isZero() + ? ethToOutputRate.times(fee) + : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); // targetInput can be less than the order size // whilst the penalty is constant, it affects the adjusted output // only up until the target has been exhausted. @@ -76,7 +80,7 @@ function nativeOrdersToPath( // scale the clipped output inline with the input const clippedOutput = clippedInput.dividedBy(input).times(output); const adjustedOutput = - side === MarketOperation.Sell ? clippedOutput.minus(penalty) : clippedOutput.plus(penalty); + side === MarketOperation.Sell ? clippedOutput.minus(outputPenalty) : clippedOutput.plus(outputPenalty); const adjustedRate = side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput); // Skip orders with rates that are <= 0. @@ -84,11 +88,11 @@ function nativeOrdersToPath( continue; } path.push({ - input: clippedInput, - output: clippedOutput, - rate, + sourcePathId, adjustedRate, adjustedOutput, + input: clippedInput, + output: clippedOutput, flags: 0, index: 0, // TBD parent: undefined, // TBD @@ -114,6 +118,7 @@ function dexQuotesToPaths( ): Fill[][] { const paths: Fill[][] = []; for (let quote of dexQuotes) { + const sourcePathId = hexUtils.random(); const path: Fill[] = []; // Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves // We need not worry about Kyber fills going to UniswapReserve as the input amount @@ -132,14 +137,11 @@ function dexQuotesToPaths( ? ethToOutputRate.times(fee) : ZERO_AMOUNT; const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); - const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output); - const adjustedRate = side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); path.push({ + sourcePathId, input, output, - rate, - adjustedRate, adjustedOutput, source, fillData, @@ -189,8 +191,12 @@ export function getPathAdjustedSize(path: Fill[], targetInput: BigNumber = POSIT for (const fill of path) { if (input.plus(fill.input).gte(targetInput)) { const di = targetInput.minus(input); - input = input.plus(di); - output = output.plus(fill.adjustedOutput.times(di.div(fill.input))); + if (di.gt(0)) { + input = input.plus(di); + // Penalty does not get interpolated. + const penalty = fill.adjustedOutput.minus(fill.output); + output = output.plus(fill.output.times(di.div(fill.input)).plus(penalty)); + } break; } else { input = input.plus(fill.input); @@ -219,6 +225,10 @@ export function isValidPath(path: Fill[], skipDuplicateCheck: boolean = false): } flags |= path[i].flags; } + return arePathFlagsAllowed(flags); +} + +export function arePathFlagsAllowed(flags: number): boolean { const multiBridgeConflict = FillFlags.MultiBridge | FillFlags.ConflictsWithMultiBridge; return (flags & multiBridgeConflict) !== multiBridgeConflict; } @@ -243,7 +253,7 @@ export function collapsePath(path: Fill[]): CollapsedFill[] { if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { const prevFill = collapsed[collapsed.length - 1]; // If the last fill is from the same source, merge them. - if (prevFill.source === source) { + if (prevFill.sourcePathId === fill.sourcePathId) { prevFill.input = prevFill.input.plus(fill.input); prevFill.output = prevFill.output.plus(fill.output); prevFill.subFills.push(fill); @@ -251,6 +261,7 @@ export function collapsePath(path: Fill[]): CollapsedFill[] { } } collapsed.push({ + sourcePathId: fill.sourcePathId, source: fill.source, fillData: fill.fillData, input: fill.input, @@ -261,35 +272,14 @@ export function collapsePath(path: Fill[]): CollapsedFill[] { return collapsed; } -export function getFallbackSourcePaths(optimalPath: Fill[], allPaths: Fill[][]): Fill[][] { - const optimalSources: ERC20BridgeSource[] = []; - for (const fill of optimalPath) { - if (!optimalSources.includes(fill.source)) { - optimalSources.push(fill.source); - } - } - const fallbackPaths: Fill[][] = []; - for (const path of allPaths) { - if (optimalSources.includes(path[0].source)) { - continue; - } - // HACK(dorothy-zbornak): We *should* be filtering out paths that - // conflict with the optimal path (i.e., Kyber conflicts), but in - // practice we often end up not being able to find a fallback path - // because we've lost 2 major liquiduty sources. The end result is - // we end up with many more reverts than what would be actually caused - // by conflicts. - fallbackPaths.push(path); - } - return fallbackPaths; +export function getPathAdjustedCompleteRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { + const [input, output] = getPathAdjustedSize(path, targetInput); + return getCompleteRate(side, input, output, targetInput); } export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { const [input, output] = getPathAdjustedSize(path, targetInput); - if (input.eq(0) || output.eq(0)) { - return ZERO_AMOUNT; - } - return side === MarketOperation.Sell ? output.div(input) : input.div(output); + return getRate(side, input, output); } export function getPathAdjustedSlippage( @@ -305,3 +295,29 @@ export function getPathAdjustedSlippage( const rateChange = maxRate.minus(totalRate); return rateChange.div(maxRate).toNumber(); } + +export function getCompleteRate( + side: MarketOperation, + input: BigNumber, + output: BigNumber, + targetInput: BigNumber, +): BigNumber { + if (input.eq(0) || output.eq(0) || targetInput.eq(0)) { + return ZERO_AMOUNT; + } + // Penalize paths that fall short of the entire input amount by a factor of + // input / targetInput => (i / t) + if (side === MarketOperation.Sell) { + // (o / i) * (i / t) => (o / t) + return output.div(targetInput); + } + // (i / o) * (i / t) + return input.div(output).times(input.div(targetInput)); +} + +export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { + if (input.eq(0) || output.eq(0)) { + return ZERO_AMOUNT; + } + return side === MarketOperation.Sell ? output.div(input) : input.div(output); +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 597dcb0fc1..85bfef8633 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -1,11 +1,15 @@ import { ContractAddresses } from '@0x/contract-addresses'; +import { ZERO_AMOUNT } from '@0x/order-utils'; import { RFQTIndicativeQuote } from '@0x/quote-server'; import { SignedOrder } from '@0x/types'; import { BigNumber, NULL_ADDRESS } from '@0x/utils'; +import * as _ from 'lodash'; import { MarketOperation } from '../../types'; +import { QuoteRequestor } from '../quote_requestor'; import { difference } from '../utils'; +import { QuoteReportGenerator } from './../quote_report_generator'; import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants'; import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; import { @@ -22,7 +26,9 @@ import { ERC20BridgeSource, FeeSchedule, GetMarketOrdersOpts, + MarketSideLiquidity, OptimizedMarketOrder, + OptimizedOrdersAndQuoteReport, OrderDomain, } from './types'; @@ -70,18 +76,17 @@ export class MarketOperationUtils { } /** - * gets the orders required for a market sell operation by (potentially) merging native orders with - * generated bridge orders. + * Gets the liquidity available for a market sell operation * @param nativeOrders Native orders. * @param takerAmount Amount of taker asset to sell. * @param opts Options object. - * @return orders. + * @return MarketSideLiquidity. */ - public async getMarketSellOrdersAsync( + public async getMarketSellLiquidityAsync( nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { if (nativeOrders.length === 0) { throw new Error(AggregationError.EmptyOrders); } @@ -110,6 +115,17 @@ export class MarketOperationUtils { this._liquidityProviderRegistry, this._multiBridge, ), + // Get ETH -> taker token price. + await DexOrderSampler.ops.getMedianSellRateAsync( + difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), + takerToken, + this._wethAddress, + ONE_ETHER, + this._wethAddress, + this._sampler.balancerPoolsCache, + this._liquidityProviderRegistry, + this._multiBridge, + ), // Get sell quotes for taker -> maker. await DexOrderSampler.ops.getSellQuotesAsync( difference( @@ -148,44 +164,45 @@ export class MarketOperationUtils { .then(async r => this._sampler.executeAsync(r)); const [ - [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes], + [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], rfqtIndicativeQuotes, [balancerQuotes], ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); - return this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes: dexQuotes.concat(balancerQuotes), - rfqtIndicativeQuotes, - liquidityProviderAddress, - multiBridgeAddress: this._multiBridge, - inputToken: takerToken, - outputToken: makerToken, + + // Attach the LiquidityProvider address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach( + q => (q.fillData = { poolAddress: liquidityProviderAddress }), + ); + // Attach the MultiBridge address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( + q => (q.fillData = { poolAddress: this._multiBridge }), + ); + return { side: MarketOperation.Sell, inputAmount: takerAmount, + inputToken: takerToken, + outputToken: makerToken, + dexQuotes: dexQuotes.concat(balancerQuotes), + nativeOrders, + orderFillableAmounts, ethToOutputRate: ethToMakerAssetRate, - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - }); + ethToInputRate: ethToTakerAssetRate, + rfqtIndicativeQuotes, + }; } /** - * gets the orders required for a market buy operation by (potentially) merging native orders with - * generated bridge orders. + * Gets the liquidity available for a market buy operation * @param nativeOrders Native orders. * @param makerAmount Amount of maker asset to buy. * @param opts Options object. - * @return orders. + * @return MarketSideLiquidity. */ - public async getMarketBuyOrdersAsync( + public async getMarketBuyLiquidityAsync( nativeOrders: SignedOrder[], makerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { if (nativeOrders.length === 0) { throw new Error(AggregationError.EmptyOrders); } @@ -203,6 +220,17 @@ export class MarketOperationUtils { makerToken, takerToken, ), + // Get ETH -> maker token price. + await DexOrderSampler.ops.getMedianSellRateAsync( + difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), + makerToken, + this._wethAddress, + ONE_ETHER, + this._wethAddress, + this._sampler.balancerPoolsCache, + this._liquidityProviderRegistry, + this._multiBridge, + ), // Get ETH -> taker token price. await DexOrderSampler.ops.getMedianSellRateAsync( difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), @@ -251,29 +279,81 @@ export class MarketOperationUtils { _opts, ); const [ - [orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes], + [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], rfqtIndicativeQuotes, [balancerQuotes], ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); - - return this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes: dexQuotes.concat(balancerQuotes), - rfqtIndicativeQuotes, - liquidityProviderAddress, - multiBridgeAddress: this._multiBridge, - inputToken: makerToken, - outputToken: takerToken, + // Attach the LiquidityProvider address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach( + q => (q.fillData = { poolAddress: liquidityProviderAddress }), + ); + // Attach the MultiBridge address to the sample fillData + (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( + q => (q.fillData = { poolAddress: this._multiBridge }), + ); + return { side: MarketOperation.Buy, inputAmount: makerAmount, + inputToken: makerToken, + outputToken: takerToken, + dexQuotes: dexQuotes.concat(balancerQuotes), + nativeOrders, + orderFillableAmounts, ethToOutputRate: ethToTakerAssetRate, + ethToInputRate: ethToMakerAssetRate, + rfqtIndicativeQuotes, + }; + } + + /** + * gets the orders required for a market sell operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. + * @param takerAmount Amount of taker asset to sell. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ + public async getMarketSellOrdersAsync( + nativeOrders: SignedOrder[], + takerAmount: BigNumber, + opts?: Partial, + ): Promise { + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); + return this._generateOptimizedOrdersAsync(marketSideLiquidity, { bridgeSlippage: _opts.bridgeSlippage, maxFallbackSlippage: _opts.maxFallbackSlippage, excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, + quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, + }); + } + + /** + * gets the orders required for a market buy operation by (potentially) merging native orders with + * generated bridge orders. + * @param nativeOrders Native orders. + * @param makerAmount Amount of maker asset to buy. + * @param opts Options object. + * @return object with optimized orders and a QuoteReport + */ + public async getMarketBuyOrdersAsync( + nativeOrders: SignedOrder[], + makerAmount: BigNumber, + opts?: Partial, + ): Promise { + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); + return this._generateOptimizedOrdersAsync(marketSideLiquidity, { + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, + quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, }); } @@ -333,6 +413,7 @@ export class MarketOperationUtils { const batchOrderFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][]; const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; + const ethToInputRate = ZERO_AMOUNT; return Promise.all( batchNativeOrders.map(async (nativeOrders, i) => { @@ -345,23 +426,29 @@ export class MarketOperationUtils { const dexQuotes = batchDexQuotes[i]; const makerAmount = makerAmounts[i]; try { - return await this._generateOptimizedOrdersAsync({ - orderFillableAmounts, - nativeOrders, - dexQuotes, - rfqtIndicativeQuotes: [], - inputToken: makerToken, - outputToken: takerToken, - side: MarketOperation.Buy, - inputAmount: makerAmount, - ethToOutputRate: ethToTakerAssetRate, - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - }); + const { optimizedOrders } = await this._generateOptimizedOrdersAsync( + { + side: MarketOperation.Buy, + nativeOrders, + orderFillableAmounts, + dexQuotes, + inputAmount: makerAmount, + ethToOutputRate: ethToTakerAssetRate, + ethToInputRate, + rfqtIndicativeQuotes: [], + inputToken: makerToken, + outputToken: takerToken, + }, + { + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, + }, + ); + return optimizedOrders; } catch (e) { // It's possible for one of the pairs to have no path // rather than throw NO_OPTIMAL_PATH we return undefined @@ -371,39 +458,44 @@ export class MarketOperationUtils { ); } - private async _generateOptimizedOrdersAsync(opts: { - side: MarketOperation; - inputToken: string; - outputToken: string; - inputAmount: BigNumber; - nativeOrders: SignedOrder[]; - orderFillableAmounts: BigNumber[]; - dexQuotes: DexSample[][]; - rfqtIndicativeQuotes: RFQTIndicativeQuote[]; - runLimit?: number; - ethToOutputRate?: BigNumber; - bridgeSlippage?: number; - maxFallbackSlippage?: number; - excludedSources?: ERC20BridgeSource[]; - feeSchedule?: FeeSchedule; - allowFallback?: boolean; - shouldBatchBridgeOrders?: boolean; - liquidityProviderAddress?: string; - multiBridgeAddress?: string; - }): Promise { - const { inputToken, outputToken, side, inputAmount } = opts; + private async _generateOptimizedOrdersAsync( + marketSideLiquidity: MarketSideLiquidity, + opts: { + runLimit?: number; + bridgeSlippage?: number; + maxFallbackSlippage?: number; + excludedSources?: ERC20BridgeSource[]; + feeSchedule?: FeeSchedule; + allowFallback?: boolean; + shouldBatchBridgeOrders?: boolean; + quoteRequestor?: QuoteRequestor; + }, + ): Promise { + const { + inputToken, + outputToken, + side, + inputAmount, + nativeOrders, + orderFillableAmounts, + rfqtIndicativeQuotes, + dexQuotes, + ethToOutputRate, + ethToInputRate, + } = marketSideLiquidity; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; // Convert native orders and dex quotes into fill paths. const paths = createFillPaths({ side, // Augment native orders with their fillable amounts. orders: [ - ...createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts), - ...createSignedOrdersFromRfqtIndicativeQuotes(opts.rfqtIndicativeQuotes), + ...createSignedOrdersWithFillableAmounts(side, nativeOrders, orderFillableAmounts), + ...createSignedOrdersFromRfqtIndicativeQuotes(rfqtIndicativeQuotes), ], - dexQuotes: opts.dexQuotes, + dexQuotes, targetInput: inputAmount, - ethToOutputRate: opts.ethToOutputRate, + ethToOutputRate, + ethToInputRate, excludedSources: opts.excludedSources, feeSchedule: opts.feeSchedule, }); @@ -444,17 +536,24 @@ export class MarketOperationUtils { optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; } } - return createOrdersFromPath(optimalPath, { + const optimizedOrders = createOrdersFromPath(optimalPath, { side, inputToken, outputToken, orderDomain: this._orderDomain, contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, - liquidityProviderAddress: opts.liquidityProviderAddress, - multiBridgeAddress: opts.multiBridgeAddress, shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }); + const quoteReport = new QuoteReportGenerator( + side, + _.flatten(dexQuotes), + nativeOrders, + orderFillableAmounts, + _.flatten(optimizedOrders.map(o => o.fills)), + opts.quoteRequestor, + ).generateReport(); + return { optimizedOrders, quoteReport }; } private _optionalSources(): ERC20BridgeSource[] { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index abeba32e73..f07a61a0a7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -24,6 +24,8 @@ import { CurveFillData, ERC20BridgeSource, Fill, + LiquidityProviderFillData, + MultiBridgeFillData, NativeCollapsedFill, OptimizedMarketOrder, OrderDomain, @@ -143,8 +145,6 @@ export interface CreateOrderFromPathOpts { contractAddresses: ContractAddresses; bridgeSlippage: number; shouldBatchBridgeOrders: boolean; - liquidityProviderAddress?: string; - multiBridgeAddress?: string; } // Convert sell fills into orders. @@ -177,7 +177,8 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts return orders; } -function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrderFromPathOpts): string { +function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string { + const source = fill.source; switch (source) { case ERC20BridgeSource.Eth2Dai: return opts.contractAddresses.eth2DaiBridge; @@ -192,15 +193,9 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder case ERC20BridgeSource.Balancer: return opts.contractAddresses.balancerBridge; case ERC20BridgeSource.LiquidityProvider: - if (opts.liquidityProviderAddress === undefined) { - throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.'); - } - return opts.liquidityProviderAddress; + return (fill.fillData as LiquidityProviderFillData).poolAddress; case ERC20BridgeSource.MultiBridge: - if (opts.multiBridgeAddress === undefined) { - throw new Error('Cannot create a MultiBridge order without a MultiBridge address.'); - } - return opts.multiBridgeAddress; + return (fill.fillData as MultiBridgeFillData).poolAddress; default: break; } @@ -209,7 +204,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { const [makerToken, takerToken] = getMakerTakerTokens(opts); - const bridgeAddress = getBridgeAddressFromSource(fill.source, opts); + const bridgeAddress = getBridgeAddressFromFill(fill, opts); let makerAssetData; switch (fill.source) { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts index f4870e5d2f..f649c59e07 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts @@ -3,16 +3,20 @@ import { BigNumber } from '@0x/utils'; import { MarketOperation } from '../../types'; import { ZERO_AMOUNT } from './constants'; -import { getPathAdjustedSize, getPathSize, isValidPath } from './fills'; +import { + arePathFlagsAllowed, + getCompleteRate, + getPathAdjustedCompleteRate, + getPathAdjustedRate, + getPathAdjustedSize, + getPathSize, + isValidPath, +} from './fills'; import { Fill } from './types'; -// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs +// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise -const RUN_LIMIT_DECAY_FACTOR = 0.8; -// Used to yield the event loop when performing CPU intensive tasks -// tislint:disable-next-line:no-inferred-empty-object-type -const setImmediateAsync = async (delay: number = 0) => - new Promise(resolve => setImmediate(() => resolve(), delay)); +const RUN_LIMIT_DECAY_FACTOR = 0.5; /** * Find the optimal mixture of paths that maximizes (for sells) or minimizes @@ -22,16 +26,21 @@ export async function findOptimalPathAsync( side: MarketOperation, paths: Fill[][], targetInput: BigNumber, - runLimit: number = 2 ** 15, + runLimit: number = 2 ** 8, ): Promise { - // Sort paths in descending order by adjusted output amount. + // Sort paths by descending adjusted completed rate. const sortedPaths = paths .slice(0) - .sort((a, b) => getPathAdjustedSize(b, targetInput)[1].comparedTo(getPathAdjustedSize(a, targetInput)[1])); + .sort((a, b) => + getPathAdjustedCompleteRate(side, b, targetInput).comparedTo( + getPathAdjustedCompleteRate(side, a, targetInput), + ), + ); let optimalPath = sortedPaths[0] || []; for (const [i, path] of sortedPaths.slice(1).entries()) { optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i); - await setImmediateAsync(); + // Yield to event loop. + await Promise.resolve(); } return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; } @@ -43,10 +52,12 @@ function mixPaths( targetInput: BigNumber, maxSteps: number, ): Fill[] { - let bestPath: Fill[] = []; - let bestPathInput = ZERO_AMOUNT; - let bestPathRate = ZERO_AMOUNT; + const _maxSteps = Math.max(maxSteps, 32); let steps = 0; + // We assume pathA is the better of the two initially. + let bestPath: Fill[] = pathA; + let [bestPathInput, bestPathOutput] = getPathAdjustedSize(pathA, targetInput); + let bestPathRate = getCompleteRate(side, bestPathInput, bestPathOutput, targetInput); const _isBetterPath = (input: BigNumber, rate: BigNumber) => { if (bestPathInput.lt(targetInput)) { return input.gt(bestPathInput); @@ -55,46 +66,77 @@ function mixPaths( } return false; }; - const _walk = (path: Fill[], input: BigNumber, output: BigNumber, allFills: Fill[]) => { + const _walk = (path: Fill[], input: BigNumber, output: BigNumber, flags: number, remainingFills: Fill[]) => { steps += 1; - const rate = getRate(side, input, output); + const rate = getCompleteRate(side, input, output, targetInput); if (_isBetterPath(input, rate)) { bestPath = path; bestPathInput = input; + bestPathOutput = output; bestPathRate = rate; } const remainingInput = targetInput.minus(input); if (remainingInput.gt(0)) { - for (let i = 0; i < allFills.length; ++i) { - const fill = allFills[i]; - if (steps + 1 >= maxSteps) { - break; - } - const childPath = [...path, fill]; - if (!isValidPath(childPath, true)) { + for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) { + const fill = remainingFills[i]; + // Only walk valid paths. + if (!isValidNextPathFill(path, flags, fill)) { continue; } // Remove this fill from the next list of candidate fills. - const nextAllFills = allFills.slice(); - nextAllFills.splice(i, 1); + const nextRemainingFills = remainingFills.slice(); + nextRemainingFills.splice(i, 1); // Recurse. _walk( - childPath, + [...path, fill], input.plus(BigNumber.min(remainingInput, fill.input)), output.plus( // Clip the output of the next fill to the remaining // input. clipFillAdjustedOutput(fill, remainingInput), ), - nextAllFills, + flags | fill.flags, + nextRemainingFills, ); } } }; - _walk(bestPath, ZERO_AMOUNT, ZERO_AMOUNT, [...pathA, ...pathB].sort((a, b) => b.rate.comparedTo(a.rate))); + const allFills = [...pathA, ...pathB]; + const sources = allFills.filter(f => f.index === 0).map(f => f.sourcePathId); + const rateBySource = Object.assign( + {}, + ...sources.map(s => ({ + [s]: getPathAdjustedRate(side, allFills.filter(f => f.sourcePathId === s), targetInput), + })), + ); + // Sort subpaths by rate and keep fills contiguous to improve our + // chances of walking ideal, valid paths first. + const sortedFills = allFills.sort((a, b) => { + if (a.sourcePathId !== b.sourcePathId) { + return rateBySource[b.sourcePathId].comparedTo(rateBySource[a.sourcePathId]); + } + return a.index - b.index; + }); + _walk([], ZERO_AMOUNT, ZERO_AMOUNT, 0, sortedFills); + if (!isValidPath(bestPath)) { + throw new Error('nooope'); + } return bestPath; } +function isValidNextPathFill(path: Fill[], pathFlags: number, fill: Fill): boolean { + if (path.length === 0) { + return !fill.parent; + } + if (path[path.length - 1] === fill.parent) { + return true; + } + if (fill.parent) { + return false; + } + return arePathFlagsAllowed(pathFlags | fill.flags); +} + function isPathComplete(path: Fill[], targetInput: BigNumber): boolean { const [input] = getPathSize(path); return input.gte(targetInput); @@ -104,16 +146,7 @@ function clipFillAdjustedOutput(fill: Fill, remainingInput: BigNumber): BigNumbe if (fill.input.lte(remainingInput)) { return fill.adjustedOutput; } + // Penalty does not get interpolated. const penalty = fill.adjustedOutput.minus(fill.output); - return remainingInput.times(fill.rate).plus(penalty); -} - -function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { - if (input.eq(0) || output.eq(0)) { - return ZERO_AMOUNT; - } - if (side === MarketOperation.Sell) { - return output.div(input); - } - return input.div(output); + return remainingInput.times(fill.output.div(fill.input)).plus(penalty); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 05709676c7..1f2c500ac9 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -1,8 +1,11 @@ import { ERC20BridgeSamplerContract } from '@0x/contract-wrappers'; +import { RFQTIndicativeQuote } from '@0x/quote-server'; +import { MarketOperation, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { QuoteRequestor } from '../../utils/quote_requestor'; +import { QuoteReport } from '../quote_report_generator'; /** * Order domain keys: chainId and exchange @@ -85,6 +88,14 @@ export interface UniswapV2FillData extends FillData { tokenAddressPath: string[]; } +export interface LiquidityProviderFillData extends FillData { + poolAddress: string; +} + +export interface MultiBridgeFillData extends FillData { + poolAddress: string; +} + /** * Represents an individual DEX sample from the sampler contract. */ @@ -109,16 +120,16 @@ export enum FillFlags { * Represents a node on a fill path. */ export interface Fill { + // Unique ID of the original source path this fill belongs to. + // This is generated when the path is generated and is useful to distinguish + // paths that have the same `source` IDs but are distinct (e.g., Curves). + sourcePathId: string; // See `FillFlags`. flags: FillFlags; // Input fill amount (taker asset amount in a sell, maker asset amount in a buy). input: BigNumber; // Output fill amount (maker asset amount in a sell, taker asset amount in a buy). output: BigNumber; - // The maker/taker rate. - rate: BigNumber; - // The maker/taker rate, adjusted by fees. - adjustedRate: BigNumber; // The output fill amount, ajdusted by fees. adjustedOutput: BigNumber; // Fill that must precede this one. This enforces certain fills to be contiguous. @@ -136,6 +147,10 @@ export interface Fill { * Represents continguous fills on a path that have been merged together. */ export interface CollapsedFill { + // Unique ID of the original source path this fill belongs to. + // This is generated when the path is generated and is useful to distinguish + // paths that have the same `source` IDs but are distinct (e.g., Curves). + sourcePathId: string; /** * The source DEX. */ @@ -254,3 +269,28 @@ export interface SourceQuoteOperation ext source: ERC20BridgeSource; fillData?: TFillData; } + +export interface OptimizedOrdersAndQuoteReport { + optimizedOrders: OptimizedMarketOrder[]; + quoteReport: QuoteReport; +} + +export type MarketDepthSide = Array>>; + +export interface MarketDepth { + bids: MarketDepthSide; + asks: MarketDepthSide; +} + +export interface MarketSideLiquidity { + side: MarketOperation; + inputAmount: BigNumber; + inputToken: string; + outputToken: string; + dexQuotes: Array>>; + nativeOrders: SignedOrder[]; + orderFillableAmounts: BigNumber[]; + ethToOutputRate: BigNumber; + ethToInputRate: BigNumber; + rfqtIndicativeQuotes: RFQTIndicativeQuote[]; +} diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts new file mode 100644 index 0000000000..3ea6f45e8a --- /dev/null +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -0,0 +1,161 @@ +import { orderHashUtils } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ERC20BridgeSource, SignedOrder } from '..'; +import { MarketOperation } from '../types'; + +import { CollapsedFill, DexSample, NativeCollapsedFill } from './market_operation_utils/types'; +import { QuoteRequestor } from './quote_requestor'; + +export interface BridgeReportSource { + liquiditySource: Exclude; + makerAmount: BigNumber; + takerAmount: BigNumber; +} + +interface NativeReportSourceBase { + liquiditySource: ERC20BridgeSource.Native; + makerAmount: BigNumber; + takerAmount: BigNumber; + orderHash: string; + nativeOrder: SignedOrder; + fillableTakerAmount: BigNumber; +} +export interface NativeOrderbookReportSource extends NativeReportSourceBase { + isRfqt: false; +} +export interface NativeRFQTReportSource extends NativeReportSourceBase { + isRfqt: true; + makerUri: string; +} +export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource; + +export interface QuoteReport { + sourcesConsidered: QuoteReportSource[]; + sourcesDelivered: QuoteReportSource[]; +} + +const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefined => { + // Cast as NativeCollapsedFill and then check + // if it really is a NativeCollapsedFill + const possibleNativeCollapsedFill = cf as NativeCollapsedFill; + if (possibleNativeCollapsedFill.fillData && possibleNativeCollapsedFill.fillData.order) { + return possibleNativeCollapsedFill.fillData.order; + } else { + return undefined; + } +}; + +export class QuoteReportGenerator { + private readonly _dexQuotes: DexSample[]; + private readonly _nativeOrders: SignedOrder[]; + private readonly _orderHashesToFillableAmounts: { [orderHash: string]: BigNumber }; + private readonly _marketOperation: MarketOperation; + private readonly _collapsedFills: CollapsedFill[]; + private readonly _quoteRequestor?: QuoteRequestor; + + constructor( + marketOperation: MarketOperation, + dexQuotes: DexSample[], + nativeOrders: SignedOrder[], + orderFillableAmounts: BigNumber[], + collapsedFills: CollapsedFill[], + quoteRequestor?: QuoteRequestor, + ) { + this._dexQuotes = dexQuotes; + this._nativeOrders = nativeOrders; + this._marketOperation = marketOperation; + this._quoteRequestor = quoteRequestor; + this._collapsedFills = collapsedFills; + + // convert order fillable amount array to easy to look up hash + if (orderFillableAmounts.length !== nativeOrders.length) { + // length mismatch, abort + this._orderHashesToFillableAmounts = {}; + return; + } + const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {}; + nativeOrders.forEach((nativeOrder, idx) => { + orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx]; + }); + this._orderHashesToFillableAmounts = orderHashesToFillableAmounts; + } + + public generateReport(): QuoteReport { + const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq)); + const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no)); + + const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered]; + const sourcesDelivered = this._collapsedFills.map(collapsedFill => { + const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill); + if (foundNativeOrder) { + return this._nativeOrderToReportSource(foundNativeOrder); + } else { + return this._dexSampleToReportSource(collapsedFill); + } + }); + + return { + sourcesConsidered, + sourcesDelivered, + }; + } + + private _dexSampleToReportSource(ds: DexSample): BridgeReportSource { + const liquiditySource = ds.source; + + if (liquiditySource === ERC20BridgeSource.Native) { + throw new Error(`Unexpected liquidity source Native`); + } + + // input and output map to different values + // based on the market operation + if (this._marketOperation === MarketOperation.Buy) { + return { + makerAmount: ds.input, + takerAmount: ds.output, + liquiditySource, + }; + } else if (this._marketOperation === MarketOperation.Sell) { + return { + makerAmount: ds.output, + takerAmount: ds.input, + liquiditySource, + }; + } else { + throw new Error(`Unexpected marketOperation ${this._marketOperation}`); + } + } + + private _nativeOrderToReportSource(nativeOrder: SignedOrder): NativeRFQTReportSource | NativeOrderbookReportSource { + const orderHash = orderHashUtils.getOrderHash(nativeOrder); + + const nativeOrderBase: NativeReportSourceBase = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: nativeOrder.makerAssetAmount, + takerAmount: nativeOrder.takerAssetAmount, + fillableTakerAmount: this._orderHashesToFillableAmounts[orderHash], + nativeOrder, + orderHash, + }; + + // if we find this is an rfqt order, label it as such and associate makerUri + const foundRfqtMakerUri = this._quoteRequestor && this._quoteRequestor.getMakerUriForOrderHash(orderHash); + if (foundRfqtMakerUri) { + const rfqtSource: NativeRFQTReportSource = { + ...nativeOrderBase, + isRfqt: true, + makerUri: foundRfqtMakerUri, + }; + return rfqtSource; + } else { + // if it's not an rfqt order, treat as normal + const regularNativeOrder: NativeOrderbookReportSource = { + ...nativeOrderBase, + isRfqt: false, + }; + return regularNativeOrder; + } + } +} diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 6b4e6df53f..f466714e7f 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -1,9 +1,9 @@ import { schemas, SchemaValidator } from '@0x/json-schemas'; -import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; +import { assetDataUtils, orderCalculationUtils, orderHashUtils, SignedOrder } from '@0x/order-utils'; import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; import { ERC20AssetData } from '@0x/types'; import { BigNumber, logUtils } from '@0x/utils'; -import Axios, { AxiosInstance, AxiosResponse } from 'axios'; +import Axios, { AxiosInstance } from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; @@ -92,6 +92,7 @@ export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; export class QuoteRequestor { private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); + private readonly _orderHashToMakerUri: { [orderHash: string]: string } = {}; constructor( private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, @@ -112,7 +113,7 @@ export class QuoteRequestor { const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; assertTakerAddressOrThrow(_opts.takerAddress); - const firmQuotes = await this._getQuotesAsync( // not yet BigNumber + const firmQuoteResponses = await this._getQuotesAsync( // not yet BigNumber makerAssetData, takerAssetData, assetFillAmount, @@ -121,41 +122,38 @@ export class QuoteRequestor { 'firm', ); - const ordersWithStringInts = firmQuotes.map(quote => quote.signedOrder); + const result: RFQTFirmQuote[] = []; + firmQuoteResponses.forEach(firmQuoteResponse => { + const orderWithStringInts = firmQuoteResponse.response.signedOrder; - const validatedOrdersWithStringInts = ordersWithStringInts.filter(order => { try { - const hasValidSchema = this._schemaValidator.isValid(order, schemas.signedOrderSchema); + const hasValidSchema = this._schemaValidator.isValid(orderWithStringInts, schemas.signedOrderSchema); if (!hasValidSchema) { - throw new Error('order not valid'); + throw new Error('Order not valid'); } } catch (err) { - this._warningLogger(order, `Invalid RFQ-t order received, filtering out. ${err.message}`); - return false; + this._warningLogger(orderWithStringInts, `Invalid RFQ-t order received, filtering out. ${err.message}`); + return; } if ( !hasExpectedAssetData( makerAssetData, takerAssetData, - order.makerAssetData.toLowerCase(), - order.takerAssetData.toLowerCase(), + orderWithStringInts.makerAssetData.toLowerCase(), + orderWithStringInts.takerAssetData.toLowerCase(), ) ) { - this._warningLogger(order, 'Unexpected asset data in RFQ-T order, filtering out'); - return false; + this._warningLogger(orderWithStringInts, 'Unexpected asset data in RFQ-T order, filtering out'); + return; } - if (order.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) { - this._warningLogger(order, 'Unexpected takerAddress in RFQ-T order, filtering out'); - return false; + if (orderWithStringInts.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) { + this._warningLogger(orderWithStringInts, 'Unexpected takerAddress in RFQ-T order, filtering out'); + return; } - return true; - }); - - const validatedOrders: SignedOrder[] = validatedOrdersWithStringInts.map(orderWithStringInts => { - return { + const orderWithBigNumberInts: SignedOrder = { ...orderWithStringInts, makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount), takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount), @@ -164,17 +162,25 @@ export class QuoteRequestor { expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds), salt: new BigNumber(orderWithStringInts.salt), }; - }); - const orders = validatedOrders.filter(order => { - if (orderCalculationUtils.willOrderExpire(order, this._expiryBufferMs / constants.ONE_SECOND_MS)) { - this._warningLogger(order, 'Expiry too soon in RFQ-T order, filtering out'); - return false; + if ( + orderCalculationUtils.willOrderExpire( + orderWithBigNumberInts, + this._expiryBufferMs / constants.ONE_SECOND_MS, + ) + ) { + this._warningLogger(orderWithBigNumberInts, 'Expiry too soon in RFQ-T order, filtering out'); + return; } - return true; - }); - return orders.map(order => ({ signedOrder: order })); + // Store makerUri for looking up later + this._orderHashToMakerUri[orderHashUtils.getOrderHash(orderWithBigNumberInts)] = firmQuoteResponse.makerUri; + + // Passed all validation, add it to result + result.push({ signedOrder: orderWithBigNumberInts }); + return; + }); + return result; } public async requestRfqtIndicativeQuotesAsync( @@ -196,7 +202,8 @@ export class QuoteRequestor { 'indicative', ); - const validResponsesWithStringInts = responsesWithStringInts.filter(response => { + const validResponsesWithStringInts = responsesWithStringInts.filter(result => { + const response = result.response; if (!this._isValidRfqtIndicativeQuoteResponse(response)) { this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out'); return false; @@ -210,7 +217,8 @@ export class QuoteRequestor { return true; }); - const validResponses = validResponsesWithStringInts.map(response => { + const validResponses = validResponsesWithStringInts.map(result => { + const response = result.response; return { ...response, makerAssetAmount: new BigNumber(response.makerAssetAmount), @@ -230,6 +238,13 @@ export class QuoteRequestor { return responses; } + /** + * Given an order hash, returns the makerUri that the order originated from + */ + public getMakerUriForOrderHash(orderHash: string): string | undefined { + return this._orderHashToMakerUri[orderHash]; + } + private _isValidRfqtIndicativeQuoteResponse(response: RFQTIndicativeQuote): boolean { const hasValidMakerAssetAmount = response.makerAssetAmount !== undefined && @@ -285,10 +300,9 @@ export class QuoteRequestor { marketOperation: MarketOperation, options: RfqtRequestOpts, quoteType: 'firm' | 'indicative', - ): Promise { - // create an array of promises for quote responses, using "undefined" - // as a placeholder for failed requests. - const responsesIfDefined: Array> = await Promise.all( + ): Promise> { + const result: Array<{ response: ResponseT; makerUri: string }> = []; + await Promise.all( Object.keys(this._rfqtAssetOfferings).map(async url => { if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { const requestParamsWithBigNumbers = { @@ -337,7 +351,7 @@ export class QuoteRequestor { }, }, }); - return response; + result.push({ response: response.data, makerUri: url }); } catch (err) { this._infoLogger({ rfqtMakerInteraction: { @@ -354,17 +368,10 @@ export class QuoteRequestor { options.apiKey } for taker address ${options.takerAddress}`, ); - return undefined; } } - return undefined; }), ); - - const responses = responsesIfDefined.filter( - (respIfDefd): respIfDefd is AxiosResponse => respIfDefd !== undefined, - ); - - return responses.map(response => response.data); + return result; } } diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 77ebff52b2..23f406edd8 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -9,7 +9,6 @@ import { MarketOperation, MarketSellSwapQuote, SwapQuote, - SwapQuoteBase, SwapQuoteInfo, SwapQuoteOrdersBreakdown, SwapQuoterError, @@ -20,6 +19,7 @@ import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_opera import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; import { isSupportedAssetDataInOrders } from './utils'; +import { QuoteReport } from './quote_report_generator'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? @@ -87,6 +87,7 @@ export class SwapQuoteCalculator { assetFillAmounts, opts, ); + const batchSwapQuotes = await Promise.all( batchSignedOrders.map(async (orders, i) => { if (orders) { @@ -120,7 +121,8 @@ export class SwapQuoteCalculator { } // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled - let resultOrders: OptimizedMarketOrder[] = []; + let optimizedOrders: OptimizedMarketOrder[] | undefined; + let quoteReport: QuoteReport | undefined; { // Scale fees by gas price. @@ -137,20 +139,24 @@ export class SwapQuoteCalculator { if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable - resultOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); + optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); } else { if (operation === MarketOperation.Buy) { - resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( + const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync( prunedOrders, assetFillAmount, _opts, ); + optimizedOrders = buyResult.optimizedOrders; + quoteReport = buyResult.quoteReport; } else { - resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( + const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( prunedOrders, assetFillAmount, _opts, ); + optimizedOrders = sellResult.optimizedOrders; + quoteReport = sellResult.quoteReport; } } } @@ -160,11 +166,12 @@ export class SwapQuoteCalculator { return createSwapQuote( makerAssetData, takerAssetData, - resultOrders, + optimizedOrders, operation, assetFillAmount, gasPrice, opts.gasSchedule, + quoteReport, ); } } @@ -172,15 +179,16 @@ export class SwapQuoteCalculator { function createSwapQuote( makerAssetData: string, takerAssetData: string, - resultOrders: OptimizedMarketOrder[], + optimizedOrders: OptimizedMarketOrder[], operation: MarketOperation, assetFillAmount: BigNumber, gasPrice: BigNumber, gasSchedule: FeeSchedule, + quoteReport?: QuoteReport, ): SwapQuote { const bestCaseFillResult = simulateBestCaseFill({ gasPrice, - orders: resultOrders, + orders: optimizedOrders, side: operation, fillAmount: assetFillAmount, opts: { gasSchedule }, @@ -188,20 +196,21 @@ function createSwapQuote( const worstCaseFillResult = simulateWorstCaseFill({ gasPrice, - orders: resultOrders, + orders: optimizedOrders, side: operation, fillAmount: assetFillAmount, opts: { gasSchedule }, }); - const quoteBase: SwapQuoteBase = { + const quoteBase = { takerAssetData, makerAssetData, gasPrice, bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), - orders: resultOrders, + orders: optimizedOrders, + quoteReport, }; if (operation === MarketOperation.Buy) { @@ -209,12 +218,14 @@ function createSwapQuote( ...quoteBase, type: MarketOperation.Buy, makerAssetFillAmount: assetFillAmount, + quoteReport, }; } else { return { ...quoteBase, type: MarketOperation.Sell, takerAssetFillAmount: assetFillAmount, + quoteReport, }; } } diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts index 156e09e905..03052506fc 100644 --- a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -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'); + }); }); }); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 93815c0b43..bbf8d39729 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -507,12 +507,13 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct asset data', async () => { - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); @@ -531,24 +532,26 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct taker amount', async () => { - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount)); expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedMakerAmount = order.fills[0].output; @@ -566,11 +569,12 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, @@ -604,11 +608,12 @@ describe('MarketOperationUtils tests', () => { getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, @@ -641,11 +646,12 @@ describe('MarketOperationUtils tests', () => { getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, @@ -666,11 +672,12 @@ describe('MarketOperationUtils tests', () => { getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, @@ -689,21 +696,16 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); - const firstSources = [ - ERC20BridgeSource.Native, - ERC20BridgeSource.Native, - ERC20BridgeSource.Native, - ERC20BridgeSource.Uniswap, - ]; - const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber]; - expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); - expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); + const firstSources = orderSources.slice(0, 4); + const secondSources = orderSources.slice(4); + expect(_.intersection(firstSources, secondSources)).to.be.length(0); }); it('does not create a fallback if below maxFallbackSlippage', async () => { @@ -715,11 +717,12 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; @@ -756,7 +759,7 @@ describe('MarketOperationUtils tests', () => { ORDER_DOMAIN, registryAddress, ); - const result = await sampler.getMarketSellOrdersAsync( + const ordersAndReport = await sampler.getMarketSellOrdersAsync( [ createOrder({ makerAssetData: assetDataUtils.encodeERC20AssetData(xAsset), @@ -766,6 +769,7 @@ describe('MarketOperationUtils tests', () => { Web3Wrapper.toBaseUnitAmount(10, 18), { excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false }, ); + const result = ordersAndReport.optimizedOrders; expect(result.length).to.eql(1); expect(result[0].makerAddress).to.eql(liquidityProviderAddress); @@ -792,7 +796,7 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { @@ -805,6 +809,7 @@ describe('MarketOperationUtils tests', () => { shouldBatchBridgeOrders: true, }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.be.length(3); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); expect(orderFillSources).to.deep.eq([ @@ -913,12 +918,13 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct asset data', async () => { - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { expect(getSourceFromAssetData(order.makerAssetData)).to.exist(''); @@ -937,24 +943,26 @@ describe('MarketOperationUtils tests', () => { }); it('generates bridge orders with correct maker amount', async () => { - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, DEFAULT_OPTS, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount)); expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT); }); it('generates bridge orders with max slippage of `bridgeSlippage`', async () => { const bridgeSlippage = _.random(0.1, true); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), FILL_AMOUNT, { ...DEFAULT_OPTS, bridgeSlippage }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { const expectedTakerAmount = order.fills[0].output; @@ -971,11 +979,12 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, @@ -1009,11 +1018,12 @@ describe('MarketOperationUtils tests', () => { getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Uniswap, @@ -1045,11 +1055,12 @@ describe('MarketOperationUtils tests', () => { getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, @@ -1067,21 +1078,16 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); - const firstSources = [ - ERC20BridgeSource.Native, - ERC20BridgeSource.Native, - ERC20BridgeSource.Native, - ERC20BridgeSource.Uniswap, - ]; - const secondSources = [ERC20BridgeSource.Eth2Dai]; - expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); - expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); + const firstSources = orderSources.slice(0, 4); + const secondSources = orderSources.slice(4); + expect(_.intersection(firstSources, secondSources)).to.be.length(0); }); it('does not create a fallback if below maxFallbackSlippage', async () => { @@ -1092,11 +1098,12 @@ describe('MarketOperationUtils tests', () => { replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; @@ -1107,12 +1114,12 @@ describe('MarketOperationUtils tests', () => { it('batches contiguous bridge sources', async () => { const rates: RatesBySource = {}; rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01]; + rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; + rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; replaceSamplerOps({ getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), }); - const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), FILL_AMOUNT, { @@ -1121,6 +1128,7 @@ describe('MarketOperationUtils tests', () => { shouldBatchBridgeOrders: true, }, ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.be.length(2); const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); expect(orderFillSources).to.deep.eq([ diff --git a/packages/asset-swapper/test/quote_report_generator_test.ts b/packages/asset-swapper/test/quote_report_generator_test.ts new file mode 100644 index 0000000000..e90b19eaf5 --- /dev/null +++ b/packages/asset-swapper/test/quote_report_generator_test.ts @@ -0,0 +1,355 @@ +// tslint:disable:custom-no-magic-numbers +import { orderHashUtils } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; +import { BigNumber, hexUtils } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; +import * as TypeMoq from 'typemoq'; + +import { MarketOperation } from '../src/types'; +import { + CollapsedFill, + DexSample, + ERC20BridgeSource, + NativeCollapsedFill, +} from '../src/utils/market_operation_utils/types'; +import { QuoteRequestor } from '../src/utils/quote_requestor'; + +import { + BridgeReportSource, + NativeOrderbookReportSource, + NativeRFQTReportSource, + QuoteReportGenerator, + QuoteReportSource, +} from './../src/utils/quote_report_generator'; +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill => { + return { + sourcePathId: hexUtils.random(), + source: ERC20BridgeSource.Native, + input: order.takerAssetAmount, + output: order.makerAssetAmount, + fillData: { + order: { + ...order, + fillableMakerAssetAmount: new BigNumber(1), + fillableTakerAssetAmount: new BigNumber(1), + fillableTakerFeeAmount: new BigNumber(1), + }, + }, + subFills: [], + }; +}; + +describe('QuoteReportGenerator', async () => { + describe('generateReport', async () => { + it('should generate report properly for sell', () => { + const marketOperation: MarketOperation = MarketOperation.Sell; + + const kyberSample1: DexSample = { + source: ERC20BridgeSource.Kyber, + input: new BigNumber(10000), + output: new BigNumber(10001), + }; + const kyberSample2: DexSample = { + source: ERC20BridgeSource.Kyber, + input: new BigNumber(10003), + output: new BigNumber(10004), + }; + const uniswapSample1: DexSample = { + source: ERC20BridgeSource.UniswapV2, + input: new BigNumber(10003), + output: new BigNumber(10004), + }; + const uniswapSample2: DexSample = { + source: ERC20BridgeSource.UniswapV2, + input: new BigNumber(10005), + output: new BigNumber(10006), + }; + const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2]; + + const orderbookOrder1FillableAmount = new BigNumber(1000); + const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder1', + takerAssetAmount: orderbookOrder1FillableAmount, + }); + const orderbookOrder2FillableAmount = new BigNumber(99); + const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder2', + takerAssetAmount: orderbookOrder2FillableAmount.plus(99), + }); + const rfqtOrder1FillableAmount = new BigNumber(100); + const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({ + signature: 'rfqtOrder1', + takerAssetAmount: rfqtOrder1FillableAmount, + }); + const rfqtOrder2FillableAmount = new BigNumber(1001); + const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({ + signature: 'rfqtOrder2', + takerAssetAmount: rfqtOrder2FillableAmount.plus(100), + }); + const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2]; + const orderFillableAmounts: BigNumber[] = [ + orderbookOrder1FillableAmount, + rfqtOrder1FillableAmount, + rfqtOrder2FillableAmount, + orderbookOrder2FillableAmount, + ]; + + // generate path + const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() }; + const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() }; + const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2); + const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2); + const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill]; + + // quote generator mock + const quoteRequestor = TypeMoq.Mock.ofType(); + quoteRequestor + .setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(orderbookOrder2))) + .returns(() => { + return undefined; + }) + .verifiable(TypeMoq.Times.atLeastOnce()); + quoteRequestor + .setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder1))) + .returns(() => { + return 'https://rfqt1.provider.club'; + }) + .verifiable(TypeMoq.Times.atLeastOnce()); + quoteRequestor + .setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder2))) + .returns(() => { + return 'https://rfqt2.provider.club'; + }) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const orderReport = new QuoteReportGenerator( + marketOperation, + dexQuotes, + nativeOrders, + orderFillableAmounts, + pathGenerated, + quoteRequestor.object, + ).generateReport(); + + const rfqtOrder1Source: NativeRFQTReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: rfqtOrder1.makerAssetAmount, + takerAmount: rfqtOrder1.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(rfqtOrder1), + nativeOrder: rfqtOrder1, + fillableTakerAmount: rfqtOrder1FillableAmount, + isRfqt: true, + makerUri: 'https://rfqt1.provider.club', + }; + const rfqtOrder2Source: NativeRFQTReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: rfqtOrder2.makerAssetAmount, + takerAmount: rfqtOrder2.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(rfqtOrder2), + nativeOrder: rfqtOrder2, + fillableTakerAmount: rfqtOrder2FillableAmount, + isRfqt: true, + makerUri: 'https://rfqt2.provider.club', + }; + const orderbookOrder1Source: NativeOrderbookReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: orderbookOrder1.makerAssetAmount, + takerAmount: orderbookOrder1.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(orderbookOrder1), + nativeOrder: orderbookOrder1, + fillableTakerAmount: orderbookOrder1FillableAmount, + isRfqt: false, + }; + const orderbookOrder2Source: NativeOrderbookReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: orderbookOrder2.makerAssetAmount, + takerAmount: orderbookOrder2.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(orderbookOrder2), + nativeOrder: orderbookOrder2, + fillableTakerAmount: orderbookOrder2FillableAmount, + isRfqt: false, + }; + const uniswap1Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.UniswapV2, + makerAmount: uniswapSample1.output, + takerAmount: uniswapSample1.input, + }; + const uniswap2Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.UniswapV2, + makerAmount: uniswapSample2.output, + takerAmount: uniswapSample2.input, + }; + const kyber1Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.Kyber, + makerAmount: kyberSample1.output, + takerAmount: kyberSample1.input, + }; + const kyber2Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.Kyber, + makerAmount: kyberSample2.output, + takerAmount: kyberSample2.input, + }; + + const expectedSourcesConsidered: QuoteReportSource[] = [ + kyber1Source, + kyber2Source, + uniswap1Source, + uniswap2Source, + orderbookOrder1Source, + rfqtOrder1Source, + rfqtOrder2Source, + orderbookOrder2Source, + ]; + + expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); + + orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { + const expectedSourceConsidered = expectedSourcesConsidered[idx]; + expect(actualSourcesConsidered).to.eql( + expectedSourceConsidered, + `sourceConsidered incorrect at index ${idx}`, + ); + }); + + const expectedSourcesDelivered: QuoteReportSource[] = [ + rfqtOrder2Source, + orderbookOrder2Source, + uniswap2Source, + kyber2Source, + ]; + expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); + orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { + const expectedSourceDelivered = expectedSourcesDelivered[idx]; + + // remove fillable values + if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { + actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ + 'fillableMakerAssetAmount', + 'fillableTakerAssetAmount', + 'fillableTakerFeeAmount', + ]) as SignedOrder; + } + + expect(actualSourceDelivered).to.eql( + expectedSourceDelivered, + `sourceDelivered incorrect at index ${idx}`, + ); + }); + + quoteRequestor.verifyAll(); + }); + it('should handle properly for buy without quoteRequestor', () => { + const marketOperation: MarketOperation = MarketOperation.Buy; + const kyberSample1: DexSample = { + source: ERC20BridgeSource.Kyber, + input: new BigNumber(10000), + output: new BigNumber(10001), + }; + const uniswapSample1: DexSample = { + source: ERC20BridgeSource.UniswapV2, + input: new BigNumber(10003), + output: new BigNumber(10004), + }; + const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1]; + + const orderbookOrder1FillableAmount = new BigNumber(1000); + const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder1', + takerAssetAmount: orderbookOrder1FillableAmount.plus(101), + }); + const orderbookOrder2FillableAmount = new BigNumber(5000); + const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder2', + takerAssetAmount: orderbookOrder2FillableAmount.plus(101), + }); + const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2]; + const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount]; + + // generate path + const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); + const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() }; + const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() }; + const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill]; + + const orderReport = new QuoteReportGenerator( + marketOperation, + dexQuotes, + nativeOrders, + orderFillableAmounts, + pathGenerated, + ).generateReport(); + + const orderbookOrder1Source: NativeOrderbookReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: orderbookOrder1.makerAssetAmount, + takerAmount: orderbookOrder1.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(orderbookOrder1), + nativeOrder: orderbookOrder1, + fillableTakerAmount: orderbookOrder1FillableAmount, + isRfqt: false, + }; + const orderbookOrder2Source: NativeOrderbookReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: orderbookOrder2.makerAssetAmount, + takerAmount: orderbookOrder2.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(orderbookOrder2), + nativeOrder: orderbookOrder2, + fillableTakerAmount: orderbookOrder2FillableAmount, + isRfqt: false, + }; + const uniswap1Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.UniswapV2, + makerAmount: uniswapSample1.input, + takerAmount: uniswapSample1.output, + }; + const kyber1Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.Kyber, + makerAmount: kyberSample1.input, + takerAmount: kyberSample1.output, + }; + + const expectedSourcesConsidered: QuoteReportSource[] = [ + kyber1Source, + uniswap1Source, + orderbookOrder1Source, + orderbookOrder2Source, + ]; + expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); + orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { + const expectedSourceConsidered = expectedSourcesConsidered[idx]; + expect(actualSourcesConsidered).to.eql( + expectedSourceConsidered, + `sourceConsidered incorrect at index ${idx}`, + ); + }); + + const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; + expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); + orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { + const expectedSourceDelivered = expectedSourcesDelivered[idx]; + + // remove fillable values + if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { + actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ + 'fillableMakerAssetAmount', + 'fillableTakerAssetAmount', + 'fillableTakerFeeAmount', + ]) as SignedOrder; + } + + expect(actualSourceDelivered).to.eql( + expectedSourceDelivered, + `sourceDelivered incorrect at index ${idx}`, + ); + }); + }); + }); +}); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index 8ec591e687..c934f23d79 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -1,6 +1,6 @@ import { constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; +import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; import { MarketOperation } from '../src/types'; @@ -155,7 +155,7 @@ describe('quote_simulation tests', async () => { signature: '0x', }; } - + const nativeSourcePathId = hexUtils.random(); function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { const inputs = subdivideAmount(input, count); const outputs = subdivideAmount(output, count); @@ -163,6 +163,7 @@ describe('quote_simulation tests', async () => { const subFillInputs = subdivideAmount(inputs[i], count); const subFillOutputs = subdivideAmount(outputs[i], count); return { + sourcePathId: nativeSourcePathId, source: ERC20BridgeSource.Native, input: inputs[i], output: outputs[i], diff --git a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts index 3fe2fe4919..a4022a5407 100644 --- a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts +++ b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts @@ -121,6 +121,7 @@ describe('swapQuoteConsumerUtils', () => { swapQuoteConsumer = new SwapQuoteConsumer(provider, { chainId, + contractAddresses, }); }); after(async () => { diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 21bfc6ae26..0031849696 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -5,6 +5,18 @@ { "note": "Update `CurveBridge` address on all networks", "pr": 2633 + }, + { + "note": "Redeploy `ERC20BridgeSampler` on Kovan", + "pr": 2644 + }, + { + "note": "Deploy `UniswapV2Bridge` on Kovan", + "pr": 2652 + }, + { + "note": "Redeploy previously unverified contracts on testnets", + "pr": 2656 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index ef3c29d503..18793fd4bb 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -47,25 +47,25 @@ } }, "3": { - "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", - "erc721Proxy": "0xe654aac058bfbf9f83fcaee7793311dd82f6ddb4", + "erc20Proxy": "0xf1ec7d0ba42f15fb5c9e3adbe86431973e44764c", + "erc721Proxy": "0x070efeb7e5ffa3d1a59d03a219539551ae60ba43", "zrxToken": "0xff67881f8d12f372d91baae9752eb3631ff0ed00", "etherToken": "0xc778417e063141139fce010982780140aa0cd5ab", "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", - "exchange": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64", + "exchange": "0x5d8c9ba74607d2cbc4176882a42d4ace891c1c00", "assetProxyOwner": "0x0000000000000000000000000000000000000000", "zeroExGovernor": "0x53993733d41a88ae86f77a18a024e5548ee26579", - "forwarder": "0x2127a60bedfba1c01857b09b8f24094049c48493", - "coordinatorRegistry": "0x403cc23e88c17c4652fb904784d1af640a6722d9", - "coordinator": "0x6ff734d96104965c9c1b0108f83abc46e6e501df", - "multiAssetProxy": "0xab8fbd189c569ccdee3a4d929bb7f557be4028f6", - "staticCallProxy": "0xe1b97e47aa3796276033a5341e884d2ba46b6ac1", - "erc1155Proxy": "0x19bb6caa3bc34d39e5a23cedfa3e6c7e7f3c931d", - "devUtils": "0x88e746ad9ab158210266e7765adbe1756c73cf84", - "zrxVault": "0xffd161026865ad8b4ab28a76840474935eec4dfa", - "staking": "0x986b588e472b712385579d172494fe2685669504", - "stakingProxy": "0xfaabcee42ab6b9c649794ac6c133711071897ee9", - "erc20BridgeProxy": "0x599b340b5045436a99b1f0c718d30f5a0c8519dd", + "forwarder": "0xd5abddda4ba89c0120edc0ca8a95ed1ad0bf9fc3", + "coordinatorRegistry": "0xf8becacec90bfc361c0a2c720839e08405a72f6d", + "coordinator": "0xc2e2f8faf4bf649123b6f94103646cb4a0331006", + "multiAssetProxy": "0x7b70a148e20b348c320208df84fdd642aab49fd0", + "staticCallProxy": "0xaa460127562482faa5df42f2c39a025cd4a1cc0a", + "erc1155Proxy": "0x7f10d80f2659aaae790ab03da12be11c4e6008c3", + "devUtils": "0xc812af3f3fbc62f76ea4262576ec0f49db8b7f1c", + "zrxVault": "0x38bbb9fb54a6b6d0376948bf3b2a7ed1e8aea6e8", + "staking": "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01", + "stakingProxy": "0x6acab4c9c4e3a0c78435fdb5ad1719c95460a668", + "erc20BridgeProxy": "0xb344afed348de15eb4a9e180205a2b0739628339", "uniswapBridge": "0x0000000000000000000000000000000000000000", "uniswapV2Bridge": "0x0000000000000000000000000000000000000000", "eth2DaiBridge": "0x0000000000000000000000000000000000000000", @@ -74,11 +74,11 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0xd4690a51044db77d91d7aa8f7a3a5ad5da331af0", - "broker": "0x4aa817c6f383c8e8ae77301d18ce48efb16fd2be", + "broker": "0x4022e3982f326455f0905de3dbc4449999baf2dc", "chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74", "curveBridge": "0x1796cd592d19e3bcd744fbb025bb61a6d8cb2c09", "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", - "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", + "dexForwarderBridge": "0x3261ea1411a1a840aed708896f779e1b837c917e", "multiBridge": "0x0000000000000000000000000000000000000000", "balancerBridge": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", @@ -89,30 +89,30 @@ "transformers": { "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", - "fillQuoteTransformer": "0x43bea7eaca21a14a411274fb365707080ff25f80", + "fillQuoteTransformer": "0xfadbeff43a07dedeb69eda5590be1b78be3d1044", "affiliateFeeTransformer": "0x9d7174f55b50dad2e417bd567ad2da1ae4eef76d" } }, "4": { "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", - "exchange": "0x198805e9682fceec29413059b68550f92868c129", - "erc20Proxy": "0x2f5ae4f6106e89b4147651688a92256885c5f410", - "erc721Proxy": "0x7656d773e11ff7383a14dcf09a9c50990481cd10", + "exchange": "0xf8becacec90bfc361c0a2c720839e08405a72f6d", + "erc20Proxy": "0x070efeb7e5ffa3d1a59d03a219539551ae60ba43", + "erc721Proxy": "0x7f10d80f2659aaae790ab03da12be11c4e6008c3", "zrxToken": "0x8080c7e4b81ecf23aa6f877cfbfd9b0c228c6ffa", "etherToken": "0xc778417e063141139fce010982780140aa0cd5ab", "assetProxyOwner": "0x0000000000000000000000000000000000000000", "zeroExGovernor": "0x3f46b98061a3e1e1f41dff296ec19402c298f8a9", - "forwarder": "0x18571835c95a6d79b2f5c45b676ccd16f5fa34a1", - "coordinatorRegistry": "0x1084b6a398e47907bae43fec3ff4b677db6e4fee", - "coordinator": "0x70c5385ee5ee4629ef72abd169e888c8b4a12238", - "multiAssetProxy": "0xb34cde0ad3a83d04abebc0b66e75196f22216621", - "staticCallProxy": "0xe1b97e47aa3796276033a5341e884d2ba46b6ac1", - "erc1155Proxy": "0x19bb6caa3bc34d39e5a23cedfa3e6c7e7f3c931d", - "devUtils": "0x9402639a828bdf4e9e4103ac3b69e1a6e522eb59", - "zrxVault": "0xa5bf6ac73bc40790fc6ffc9dbbbce76c9176e224", - "staking": "0x7cbe3c09cba24f26db24d9100ee915fa9fa21f5b", - "stakingProxy": "0xc6ad5277ea225ac05e271eb14a7ebb480cd9dd9f", - "erc20BridgeProxy": "0x31b8653642110f17bdb1f719901d7e7d49b08141", + "forwarder": "0xe30f6166fe1cd5f0048abeed3d20360feb4a1fd8", + "coordinatorRegistry": "0xc2e2f8faf4bf649123b6f94103646cb4a0331006", + "coordinator": "0xf1ec7d0ba42f15fb5c9e3adbe86431973e44764c", + "multiAssetProxy": "0xb344afed348de15eb4a9e180205a2b0739628339", + "staticCallProxy": "0x7b70a148e20b348c320208df84fdd642aab49fd0", + "erc1155Proxy": "0xaa460127562482faa5df42f2c39a025cd4a1cc0a", + "devUtils": "0x46b5bc959e8a754c0256fff73bf34a52ad5cdfa9", + "zrxVault": "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01", + "staking": "0x6acab4c9c4e3a0c78435fdb5ad1719c95460a668", + "stakingProxy": "0x781ee6683595f823208be6540a279f940e6af196", + "erc20BridgeProxy": "0xa2aa4befed748fba27a3be7dfd2c4b2c6db1f49b", "uniswapBridge": "0x0000000000000000000000000000000000000000", "uniswapV2Bridge": "0x0000000000000000000000000000000000000000", "eth2DaiBridge": "0x0000000000000000000000000000000000000000", @@ -121,7 +121,7 @@ "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", - "broker": "0x0000000000000000000000000000000000000000", + "broker": "0x0dd2d6cabbd8ae7d2fe6840fa597a44b1a7e4747", "chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", "curveBridge": "0x1796cd592d19e3bcd744fbb025bb61a6d8cb2c09", "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", @@ -136,43 +136,43 @@ "transformers": { "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", - "fillQuoteTransformer": "0x2013735f6df965494a0fbc292f84dd44debaba3e", + "fillQuoteTransformer": "0x454cC891dc428Be81d1d6Fd3dd7026a752FBFBc9", "affiliateFeeTransformer": "0x9d7174f55b50dad2e417bd567ad2da1ae4eef76d" } }, "42": { - "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", - "erc721Proxy": "0x2a9127c745688a165106c11cd4d647d2220af821", + "erc20Proxy": "0xaa460127562482faa5df42f2c39a025cd4a1cc0a", + "erc721Proxy": "0x7b70a148e20b348c320208df84fdd642aab49fd0", "zrxToken": "0x2002d3812f58e35f0ea1ffbf80a75a38c32175fa", "etherToken": "0xd0a1e359811322d97991e03f863a0c30c2cf029c", "exchangeV2": "0x30589010550762d2f0d06f650d8e8b6ade6dbf4b", - "exchange": "0x4eacd0af335451709e1e7b570b8ea68edec8bc97", + "exchange": "0xf1ec7d0ba42f15fb5c9e3adbe86431973e44764c", "assetProxyOwner": "0x0000000000000000000000000000000000000000", "zeroExGovernor": "0x6ff734d96104965c9c1b0108f83abc46e6e501df", - "forwarder": "0x01c0ecf5d1a22de07a2de84c322bfa2b5435990e", - "coordinatorRegistry": "0x09fb99968c016a3ff537bf58fb3d9fe55a7975d5", - "coordinator": "0xd29e59e51e8ab5f94121efaeebd935ca4214e257", - "multiAssetProxy": "0xf6313a772c222f51c28f2304c0703b8cf5428fd8", - "staticCallProxy": "0x48e94bdb9033640d45ea7c721e25f380f8bffa43", - "erc1155Proxy": "0x64517fa2b480ba3678a2a3c0cf08ef7fd4fad36f", - "devUtils": "0x9402639a828bdf4e9e4103ac3b69e1a6e522eb59", - "zrxVault": "0xf36eabdfe986b35b62c8fd5a98a7f2aebb79b291", - "staking": "0x32b06d5611a03737a5f1834a24ccd641033fd89c", - "stakingProxy": "0xbab9145f1d57cd4bb0c9aa2d1ece0a5b6e734d34", - "erc20BridgeProxy": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64", + "forwarder": "0x0f64646a5154ae5e58b6dd87ede7b04f508d76f8", + "coordinatorRegistry": "0x070efeb7e5ffa3d1a59d03a219539551ae60ba43", + "coordinator": "0x7f10d80f2659aaae790ab03da12be11c4e6008c3", + "multiAssetProxy": "0x58a01e826e60731247e7de8b446ed4c8535a099c", + "staticCallProxy": "0xa2aa4befed748fba27a3be7dfd2c4b2c6db1f49b", + "erc1155Proxy": "0xb344afed348de15eb4a9e180205a2b0739628339", + "devUtils": "0xc67ae71928568a180b3aad1339dedcf3076876fe", + "zrxVault": "0x781ee6683595f823208be6540a279f940e6af196", + "staking": "0x73ea24041e03a012c51a45c307e0ba376af0238c", + "stakingProxy": "0xe94cb304b3f515be7c95fedcfa249a84995fd748", + "erc20BridgeProxy": "0x3577552c1fb7a44ad76beeb7ab53251668a21f8d", "uniswapBridge": "0x0e85f89f29998df65402391478e5924700c0079d", - "uniswapV2Bridge": "0x0000000000000000000000000000000000000000", + "uniswapV2Bridge": "0x7b3530a635d099de0534dc27e46cd7c57578c3c8", "eth2DaiBridge": "0x2d47147429b474d2e4f83e658015858a1312ed5b", - "erc20BridgeSampler": "0xccc9769c1a58766e79423a34b2cc5052d65c1983", + "erc20BridgeSampler": "0xcf9e66851f274aa4721e54526117876d90d51aa1", "kyberBridge": "0xaecfa25920f892b6eb496e1f6e84037f59da7f44", "chaiBridge": "0x0000000000000000000000000000000000000000", - "dydxBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", + "dydxBridge": "0xc213707de0454008758071c2edc1365621b8a5c5", "godsUnchainedValidator": "0x0000000000000000000000000000000000000000", - "broker": "0x0000000000000000000000000000000000000000", + "broker": "0xcdeb6d90ee7c96b4c713f7bb4f8604981f7ebe9d", "chainlinkStopLimit": "0x0000000000000000000000000000000000000000", "curveBridge": "0x81c0ab53a7352d2e97f682a37cba44e54647eefb", "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74", - "dexForwarderBridge": "0xf220eb0b29e18bbc8ebc964e915b7547c7b4de4f", + "dexForwarderBridge": "0x985d1a95c6a86a3bf85c4d425af984abceaf01de", "multiBridge": "0x0000000000000000000000000000000000000000", "balancerBridge": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", @@ -183,7 +183,7 @@ "transformers": { "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", - "fillQuoteTransformer": "0xbc33dd7a09da8ca943517a0fb786bcf0192f8be2", + "fillQuoteTransformer": "0xA8c8Cf29699F223766F47FE79e2B7eB1a90e08C8", "affiliateFeeTransformer": "0x9d7174f55b50dad2e417bd567ad2da1ae4eef76d" } }, diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 561d230ec1..99cfbdac98 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Update `ERC20BridgeSampler` wrapper", "pr": 2633 + }, + { + "note": "Add `exchangeProxy` to `ContractWrappers` type.", + "pr": 2649 } ] }, diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts index a7e14f3304..52665c075c 100644 --- a/packages/contract-wrappers/src/contract_wrappers.ts +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -11,6 +11,7 @@ import { ERC20TokenContract } from './generated-wrappers/erc20_token'; import { ERC721TokenContract } from './generated-wrappers/erc721_token'; import { ExchangeContract } from './generated-wrappers/exchange'; import { ForwarderContract } from './generated-wrappers/forwarder'; +import { IZeroExContract } from './generated-wrappers/i_zero_ex'; import { StakingContract } from './generated-wrappers/staking'; import { WETH9Contract } from './generated-wrappers/weth9'; import { ContractWrappersConfig } from './types'; @@ -49,6 +50,10 @@ export class ContractWrappers { * An instance of the StakingContract class containing methods for interacting with the Staking contracts. */ public staking: StakingContract; + /** + * An instance of the IZeroExContract class containing methods for interacting with the Exchange Proxy. + */ + public exchangeProxy: IZeroExContract; private readonly _web3Wrapper: Web3Wrapper; /** @@ -73,6 +78,7 @@ export class ContractWrappers { ForwarderContract, StakingContract, WETH9Contract, + IZeroExContract, ]; contractsArray.forEach(contract => { this._web3Wrapper.abiDecoder.addABI(contract.ABI(), contract.contractName); @@ -87,6 +93,7 @@ export class ContractWrappers { this.staking = new StakingContract(contractAddresses.stakingProxy, this.getProvider()); this.devUtils = new DevUtilsContract(contractAddresses.devUtils, this.getProvider()); this.coordinator = new CoordinatorContract(contractAddresses.coordinator, this.getProvider()); + this.exchangeProxy = new IZeroExContract(contractAddresses.exchangeProxy, this.getProvider()); this.contractAddresses = contractAddresses; } /** diff --git a/packages/ethereum-types/CHANGELOG.json b/packages/ethereum-types/CHANGELOG.json index c17ca5e8c5..7262e20b92 100644 --- a/packages/ethereum-types/CHANGELOG.json +++ b/packages/ethereum-types/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.3.0", + "changes": [ + { + "note": "Make `payable` field in `ConstructorAbi` optional", + "pr": 2648 + } + ] + }, { "version": "3.2.0", "changes": [ diff --git a/packages/ethereum-types/src/index.ts b/packages/ethereum-types/src/index.ts index 8565ea50ec..87be4b8e66 100644 --- a/packages/ethereum-types/src/index.ts +++ b/packages/ethereum-types/src/index.ts @@ -101,7 +101,7 @@ export interface ConstructorAbi { // from JSON files, and this value has type `string` not type `'constructor'` type: string; inputs: DataItem[]; - payable: boolean; + payable?: boolean; stateMutability: ConstructorStateMutability; } diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index 02e7c3a6e0..e2b39f9841 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Change test protocol fee to 70000.", "pr": 2637 + }, + { + "note": "Refactor `migration.ts` a little", + "pr": 2656 } ] }, diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 0646511e52..67af1f6f78 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -14,12 +14,9 @@ import { CoordinatorRegistryContract, } from '@0x/contracts-coordinator'; import { artifacts as devUtilsArtifacts, DevUtilsContract } from '@0x/contracts-dev-utils'; -import { artifacts as erc1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155'; +import { artifacts as erc1155Artifacts } from '@0x/contracts-erc1155'; import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; -import { - artifacts as erc20BridgeSamplerArtifacts, - ERC20BridgeSamplerContract, -} from '@0x/contracts-erc20-bridge-sampler'; +import { artifacts as erc20BridgeSamplerArtifacts } from '@0x/contracts-erc20-bridge-sampler'; import { artifacts as erc721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721'; import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange'; import { artifacts as forwarderArtifacts, ForwarderContract } from '@0x/contracts-exchange-forwarder'; @@ -38,8 +35,9 @@ import { ITransformERC20Contract, PayTakerTransformerContract, WethTransformerContract, + ZeroExContract, } from '@0x/contracts-zero-ex'; -import { Web3ProviderEngine } from '@0x/subproviders'; +import { Web3ProviderEngine, ZeroExProvider } from '@0x/subproviders'; import { BigNumber, providerUtils } from '@0x/utils'; import { SupportedProvider, TxData } from 'ethereum-types'; @@ -61,6 +59,53 @@ const allArtifacts = { }; const { NULL_ADDRESS } = constants; +const NULL_ADDRESSES = { + erc20Proxy: NULL_ADDRESS, + erc721Proxy: NULL_ADDRESS, + erc1155Proxy: NULL_ADDRESS, + zrxToken: NULL_ADDRESS, + etherToken: NULL_ADDRESS, + exchange: NULL_ADDRESS, + assetProxyOwner: NULL_ADDRESS, + erc20BridgeProxy: NULL_ADDRESS, + zeroExGovernor: NULL_ADDRESS, + forwarder: NULL_ADDRESS, + coordinatorRegistry: NULL_ADDRESS, + coordinator: NULL_ADDRESS, + multiAssetProxy: NULL_ADDRESS, + staticCallProxy: NULL_ADDRESS, + devUtils: NULL_ADDRESS, + exchangeV2: NULL_ADDRESS, + zrxVault: NULL_ADDRESS, + staking: NULL_ADDRESS, + stakingProxy: NULL_ADDRESS, + uniswapBridge: NULL_ADDRESS, + eth2DaiBridge: NULL_ADDRESS, + kyberBridge: NULL_ADDRESS, + erc20BridgeSampler: NULL_ADDRESS, + chaiBridge: NULL_ADDRESS, + dydxBridge: NULL_ADDRESS, + curveBridge: NULL_ADDRESS, + uniswapV2Bridge: NULL_ADDRESS, + godsUnchainedValidator: NULL_ADDRESS, + broker: NULL_ADDRESS, + chainlinkStopLimit: NULL_ADDRESS, + maximumGasPrice: NULL_ADDRESS, + dexForwarderBridge: NULL_ADDRESS, + multiBridge: NULL_ADDRESS, + balancerBridge: NULL_ADDRESS, + exchangeProxyGovernor: NULL_ADDRESS, + exchangeProxy: NULL_ADDRESS, + exchangeProxyAllowanceTarget: NULL_ADDRESS, + exchangeProxyTransformerDeployer: NULL_ADDRESS, + exchangeProxyFlashWallet: NULL_ADDRESS, + transformers: { + wethTransformer: NULL_ADDRESS, + payTakerTransformer: NULL_ADDRESS, + fillQuoteTransformer: NULL_ADDRESS, + affiliateFeeTransformer: NULL_ADDRESS, + }, +}; /** * Creates and deploys all the contracts that are required for the latest @@ -75,22 +120,8 @@ export async function runMigrationsAsync( ): Promise { const provider = providerUtils.standardizeOrThrow(supportedProvider); const chainId = new BigNumber(await providerUtils.getChainIdAsync(provider)); + const { exchangeV2 } = getContractAddressesForChainOrThrow(chainId.toNumber()); - // Proxies - const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync( - assetProxyArtifacts.ERC20Proxy, - provider, - txDefaults, - allArtifacts, - ); - const erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync( - assetProxyArtifacts.ERC721Proxy, - provider, - txDefaults, - allArtifacts, - ); - - // ZRX const zrxToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync( erc20Artifacts.DummyERC20Token, provider, @@ -110,6 +141,8 @@ export async function runMigrationsAsync( allArtifacts, ); + await _migrateDummyTokensAsync(provider, txDefaults); + // Exchange const exchange = await ExchangeContract.deployFrom0xArtifactAsync( exchangeArtifacts.Exchange, @@ -119,31 +152,135 @@ export async function runMigrationsAsync( chainId, ); - // Dummy ERC20 tokens - for (const token of erc20TokenInfo) { - const totalSupply = new BigNumber(1000000000000000000000000000); - // tslint:disable-next-line:no-unused-variable - const dummyErc20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( - erc20Artifacts.DummyERC20Token, - provider, - txDefaults, - allArtifacts, - token.name, - token.symbol, - token.decimals, - totalSupply, - ); - } - - // ERC721 - // tslint:disable-next-line:no-unused-variable - const cryptoKittieToken = await DummyERC721TokenContract.deployFrom0xArtifactAsync( - erc721Artifacts.DummyERC721Token, + // CoordinatorRegistry + const coordinatorRegistry = await CoordinatorRegistryContract.deployFrom0xArtifactAsync( + coordinatorArtifacts.CoordinatorRegistry, + provider, + txDefaults, + allArtifacts, + ); + + // Coordinator + const coordinator = await CoordinatorContract.deployFrom0xArtifactAsync( + coordinatorArtifacts.Coordinator, + provider, + txDefaults, + allArtifacts, + exchange.address, + chainId, + ); + + const [ + erc20Proxy, + erc721Proxy, + erc1155Proxy, + staticCallProxy, + multiAssetProxy, + erc20BridgeProxy, + ] = await _migrateAssetProxiesAsync(exchange, provider, txDefaults); + + // Dev Utils + const devUtils = await DevUtilsContract.deployWithLibrariesFrom0xArtifactAsync( + devUtilsArtifacts.DevUtils, + devUtilsArtifacts, + provider, + txDefaults, + allArtifacts, + exchange.address, + NULL_ADDRESS, + NULL_ADDRESS, + ); + + const [zrxVault, stakingLogic, stakingProxy] = await _migrateStakingAsync( + exchange, + erc20Proxy, + zrxToken.address, + etherToken.address, + provider, + txDefaults, + ); + + // Forwarder + // Deployed after Exchange and Staking is configured as it queries + // in the constructor + const forwarder = await ForwarderContract.deployFrom0xArtifactAsync( + forwarderArtifacts.Forwarder, + provider, + txDefaults, + allArtifacts, + exchange.address, + exchangeV2, + etherToken.address, + ); + + const [ + exchangeProxy, + fillQuoteTransformer, + payTakerTransformer, + wethTransformer, + affiliateFeeTransformer, + exchangeProxyFlashWalletAddress, + exchangeProxyAllowanceTargetAddress, + ] = await _migrateExchangeProxyAsync(exchange, etherToken.address, provider, txDefaults); + + const contractAddresses = { + ...NULL_ADDRESSES, + erc20Proxy: erc20Proxy.address, + erc721Proxy: erc721Proxy.address, + erc1155Proxy: erc1155Proxy.address, + zrxToken: zrxToken.address, + etherToken: etherToken.address, + exchange: exchange.address, + erc20BridgeProxy: erc20BridgeProxy.address, + forwarder: forwarder.address, + coordinatorRegistry: coordinatorRegistry.address, + coordinator: coordinator.address, + multiAssetProxy: multiAssetProxy.address, + staticCallProxy: staticCallProxy.address, + devUtils: devUtils.address, + zrxVault: zrxVault.address, + staking: stakingLogic.address, + stakingProxy: stakingProxy.address, + exchangeProxy: exchangeProxy.address, + exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, + exchangeProxyTransformerDeployer: txDefaults.from, + exchangeProxyFlashWallet: exchangeProxyFlashWalletAddress, + transformers: { + wethTransformer: wethTransformer.address, + payTakerTransformer: payTakerTransformer.address, + fillQuoteTransformer: fillQuoteTransformer.address, + affiliateFeeTransformer: affiliateFeeTransformer.address, + }, + }; + return contractAddresses; +} + +async function _migrateAssetProxiesAsync( + exchange: ExchangeContract, + provider: ZeroExProvider, + txDefaults: TxData, +): Promise< + [ + ERC20ProxyContract, + ERC721ProxyContract, + ERC1155ProxyContract, + StaticCallProxyContract, + MultiAssetProxyContract, + ERC20BridgeProxyContract + ] +> { + // Proxies + const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.ERC20Proxy, + provider, + txDefaults, + allArtifacts, + ); + const erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.ERC721Proxy, provider, txDefaults, allArtifacts, - erc721TokenInfo[0].name, - erc721TokenInfo[0].symbol, ); // 1155 Asset Proxy @@ -168,19 +305,29 @@ export async function runMigrationsAsync( allArtifacts, ); + const erc20BridgeProxy = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.ERC20BridgeProxy, + provider, + txDefaults, + allArtifacts, + ); + await erc20Proxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); await erc721Proxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); await erc1155Proxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); await multiAssetProxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); + await erc20BridgeProxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); // MultiAssetProxy await erc20Proxy.addAuthorizedAddress(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); await erc721Proxy.addAuthorizedAddress(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); await erc1155Proxy.addAuthorizedAddress(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); + await erc20BridgeProxy.addAuthorizedAddress(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); await multiAssetProxy.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync(txDefaults); await multiAssetProxy.registerAssetProxy(erc721Proxy.address).awaitTransactionSuccessAsync(txDefaults); await multiAssetProxy.registerAssetProxy(erc1155Proxy.address).awaitTransactionSuccessAsync(txDefaults); await multiAssetProxy.registerAssetProxy(staticCallProxy.address).awaitTransactionSuccessAsync(txDefaults); + await multiAssetProxy.registerAssetProxy(erc20BridgeProxy.address).awaitTransactionSuccessAsync(txDefaults); // Register the Asset Proxies to the Exchange await exchange.registerAssetProxy(erc20Proxy.address).awaitTransactionSuccessAsync(txDefaults); @@ -188,64 +335,26 @@ export async function runMigrationsAsync( await exchange.registerAssetProxy(erc1155Proxy.address).awaitTransactionSuccessAsync(txDefaults); await exchange.registerAssetProxy(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); await exchange.registerAssetProxy(staticCallProxy.address).awaitTransactionSuccessAsync(txDefaults); - - // CoordinatorRegistry - const coordinatorRegistry = await CoordinatorRegistryContract.deployFrom0xArtifactAsync( - coordinatorArtifacts.CoordinatorRegistry, - provider, - txDefaults, - allArtifacts, - ); - - // Coordinator - const coordinator = await CoordinatorContract.deployFrom0xArtifactAsync( - coordinatorArtifacts.Coordinator, - provider, - txDefaults, - allArtifacts, - exchange.address, - chainId, - ); - - // Dev Utils - const devUtils = await DevUtilsContract.deployWithLibrariesFrom0xArtifactAsync( - devUtilsArtifacts.DevUtils, - devUtilsArtifacts, - provider, - txDefaults, - allArtifacts, - exchange.address, - NULL_ADDRESS, - NULL_ADDRESS, - ); - - // tslint:disable-next-line:no-unused-variable - const erc1155DummyToken = await ERC1155MintableContract.deployFrom0xArtifactAsync( - erc1155Artifacts.ERC1155Mintable, - provider, - txDefaults, - allArtifacts, - ); - - const erc20BridgeProxy = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync( - assetProxyArtifacts.ERC20BridgeProxy, - provider, - txDefaults, - allArtifacts, - ); await exchange.registerAssetProxy(erc20BridgeProxy.address).awaitTransactionSuccessAsync(txDefaults); - await erc20BridgeProxy.addAuthorizedAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); - await erc20BridgeProxy.addAuthorizedAddress(multiAssetProxy.address).awaitTransactionSuccessAsync(txDefaults); - await multiAssetProxy.registerAssetProxy(erc20BridgeProxy.address).awaitTransactionSuccessAsync(txDefaults); - const zrxProxy = erc20Proxy.address; + return [erc20Proxy, erc721Proxy, erc1155Proxy, staticCallProxy, multiAssetProxy, erc20BridgeProxy]; +} + +async function _migrateStakingAsync( + exchange: ExchangeContract, + erc20Proxy: ERC20ProxyContract, + zrxTokenAddress: string, + etherTokenAddress: string, + provider: ZeroExProvider, + txDefaults: TxData, +): Promise<[ZrxVaultContract, TestStakingContract, StakingProxyContract]> { const zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync( stakingArtifacts.ZrxVault, provider, txDefaults, allArtifacts, - zrxProxy, - zrxToken.address, + erc20Proxy.address, + zrxTokenAddress, ); // Note we use TestStakingContract as the deployed bytecode of a StakingContract @@ -255,7 +364,7 @@ export async function runMigrationsAsync( provider, txDefaults, allArtifacts, - etherToken.address, + etherTokenAddress, zrxVault.address, ); @@ -281,29 +390,25 @@ export async function runMigrationsAsync( await stakingLogic.addAuthorizedAddress(txDefaults.from).awaitTransactionSuccessAsync(txDefaults); await stakingLogic.addExchangeAddress(exchange.address).awaitTransactionSuccessAsync(txDefaults); - // Forwarder - // Deployed after Exchange and Staking is configured as it queries - // in the constructor - const { exchangeV2: exchangeV2Address } = getContractAddressesForChainOrThrow(chainId.toNumber()); - const forwarder = await ForwarderContract.deployFrom0xArtifactAsync( - forwarderArtifacts.Forwarder, - provider, - txDefaults, - allArtifacts, - exchange.address, - exchangeV2Address || NULL_ADDRESS, - etherToken.address, - ); - - const erc20BridgeSampler = await ERC20BridgeSamplerContract.deployFrom0xArtifactAsync( - erc20BridgeSamplerArtifacts.ERC20BridgeSampler, - provider, - txDefaults, - allArtifacts, - ); - - // Exchange Proxy ////////////////////////////////////////////////////////// + return [zrxVault, stakingLogic, stakingProxy]; +} +async function _migrateExchangeProxyAsync( + exchange: ExchangeContract, + etherTokenAddress: string, + provider: ZeroExProvider, + txDefaults: TxData, +): Promise< + [ + ZeroExContract, + FillQuoteTransformerContract, + PayTakerTransformerContract, + WethTransformerContract, + AffiliateFeeTransformerContract, + string, + string + ] +> { const exchangeProxy = await fullMigrateExchangeProxyAsync(txDefaults.from, provider, txDefaults); const exchangeProxyAllowanceTargetAddress = await new ITokenSpenderContract( exchangeProxy.address, @@ -335,7 +440,7 @@ export async function runMigrationsAsync( provider, txDefaults, allArtifacts, - etherToken.address, + etherTokenAddress, ); const affiliateFeeTransformer = await AffiliateFeeTransformerContract.deployFrom0xArtifactAsync( exchangeProxyArtifacts.AffiliateFeeTransformer, @@ -344,54 +449,42 @@ export async function runMigrationsAsync( allArtifacts, ); - const contractAddresses = { - erc20Proxy: erc20Proxy.address, - erc721Proxy: erc721Proxy.address, - erc1155Proxy: erc1155Proxy.address, - zrxToken: zrxToken.address, - etherToken: etherToken.address, - exchange: exchange.address, - assetProxyOwner: NULL_ADDRESS, - erc20BridgeProxy: erc20BridgeProxy.address, - zeroExGovernor: NULL_ADDRESS, - forwarder: forwarder.address, - coordinatorRegistry: coordinatorRegistry.address, - coordinator: coordinator.address, - multiAssetProxy: multiAssetProxy.address, - staticCallProxy: staticCallProxy.address, - devUtils: devUtils.address, - exchangeV2: exchangeV2Address || NULL_ADDRESS, - zrxVault: zrxVault.address, - staking: stakingLogic.address, - stakingProxy: stakingProxy.address, - uniswapBridge: NULL_ADDRESS, - eth2DaiBridge: NULL_ADDRESS, - kyberBridge: NULL_ADDRESS, - erc20BridgeSampler: erc20BridgeSampler.address, - chaiBridge: NULL_ADDRESS, - dydxBridge: NULL_ADDRESS, - curveBridge: NULL_ADDRESS, - uniswapV2Bridge: NULL_ADDRESS, - godsUnchainedValidator: NULL_ADDRESS, - broker: NULL_ADDRESS, - chainlinkStopLimit: NULL_ADDRESS, - maximumGasPrice: NULL_ADDRESS, - dexForwarderBridge: NULL_ADDRESS, - multiBridge: NULL_ADDRESS, - balancerBridge: NULL_ADDRESS, - exchangeProxyGovernor: NULL_ADDRESS, - exchangeProxy: exchangeProxy.address, - exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, - exchangeProxyTransformerDeployer: txDefaults.from, - exchangeProxyFlashWallet: exchangeProxyFlashWalletAddress, - transformers: { - wethTransformer: wethTransformer.address, - payTakerTransformer: payTakerTransformer.address, - fillQuoteTransformer: fillQuoteTransformer.address, - affiliateFeeTransformer: affiliateFeeTransformer.address, - }, - }; - return contractAddresses; + return [ + exchangeProxy, + fillQuoteTransformer, + payTakerTransformer, + wethTransformer, + affiliateFeeTransformer, + exchangeProxyFlashWalletAddress, + exchangeProxyAllowanceTargetAddress, + ]; +} + +async function _migrateDummyTokensAsync(provider: ZeroExProvider, txDefaults: TxData): Promise { + // Dummy ERC20 tokens + for (const token of erc20TokenInfo) { + const totalSupply = new BigNumber(1000000000000000000000000000); + await DummyERC20TokenContract.deployFrom0xArtifactAsync( + erc20Artifacts.DummyERC20Token, + provider, + txDefaults, + allArtifacts, + token.name, + token.symbol, + token.decimals, + totalSupply, + ); + } + + // Dummy ERC721 token + await DummyERC721TokenContract.deployFrom0xArtifactAsync( + erc721Artifacts.DummyERC721Token, + provider, + txDefaults, + allArtifacts, + erc721TokenInfo[0].name, + erc721TokenInfo[0].symbol, + ); } let _cachedContractAddresses: ContractAddresses; @@ -414,3 +507,4 @@ export async function runMigrationsOnceAsync( _cachedContractAddresses = await runMigrationsAsync(provider, txDefaults); return _cachedContractAddresses; } +// tslint:disable-next-line: max-file-line-count diff --git a/packages/monorepo-scripts/src/doc_gen_configs.ts b/packages/monorepo-scripts/src/doc_gen_configs.ts index 0d0685b576..5e45b98942 100644 --- a/packages/monorepo-scripts/src/doc_gen_configs.ts +++ b/packages/monorepo-scripts/src/doc_gen_configs.ts @@ -28,6 +28,7 @@ export const docGenConfigs: DocGenConfigs = { TFillData: true, IterableIterator: true, Set: true, + Exclude: true, }, // Some types are not explicitly part of the public interface like params, return values, etc... But we still // want them exported. E.g error enum types that can be thrown by methods. These must be manually added to this diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 412b297e27..be2bacc702 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "10.3.1", + "changes": [ + { + "note": "Add gitpkg.", + "pr": 2649 + }, + { + "note": "Fix `decodeAffiliateFeeTransformerData`", + "pr": 2658 + } + ] + }, { "version": "10.3.0", "changes": [ diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index 494cb9bf6a..744c67dffb 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -10,6 +10,7 @@ "scripts": { "build": "yarn tsc -b", "build:ci": "yarn build", + "publish:private": "yarn clean && yarn build && gitpkg publish", "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", "test:circleci": "yarn test:coverage", @@ -29,6 +30,9 @@ "assets": [] } }, + "gitpkg": { + "registry": "git@github.com:0xProject/gitpkg-registry.git" + }, "license": "Apache-2.0", "repository": { "type": "git", @@ -52,6 +56,7 @@ "@types/web3-provider-engine": "^14.0.0", "chai": "^4.0.1", "ethereum-types": "^3.2.0", + "gitpkg": "https://github.com/0xProject/gitpkg.git", "make-promises-safe": "^1.1.0", "mocha": "^6.2.0", "npm-run-all": "^4.1.2", diff --git a/packages/order-utils/src/transformer_data_encoders.ts b/packages/order-utils/src/transformer_data_encoders.ts index 00a7831f61..cb3bb17d11 100644 --- a/packages/order-utils/src/transformer_data_encoders.ts +++ b/packages/order-utils/src/transformer_data_encoders.ts @@ -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); }