From 7d5e4ed68531b6df83b6dd22a3298b987747907f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 11 Feb 2022 03:23:57 -0500 Subject: [PATCH] rebasing against development... --- .../contracts/src/CompoundSampler.sol | 96 -- .../contracts/src/ERC20BridgeSampler.sol | 95 -- .../contracts/src/KyberDmmSampler.sol | 179 --- .../contracts/src/UtilitySampler.sol | 95 -- .../src/noop_samplers/AaveV2Sampler.ts | 57 - .../exchange_proxy_swap_quote_consumer.ts | 2 - .../utils/market_operation_utils/constants.ts | 3 +- .../src/utils/market_operation_utils/index.ts | 1166 ++++++++++------- .../market_operation_utils/path_optimizer.ts | 4 +- .../sampler_no_operation.ts | 36 - .../src/utils/market_operation_utils/types.ts | 36 +- .../src/utils/quote_report_generator.ts | 111 +- packages/asset-swapper/src/utils/utils.ts | 2 + yarn.lock | 12 +- 14 files changed, 865 insertions(+), 1029 deletions(-) delete mode 100644 packages/asset-swapper/contracts/src/CompoundSampler.sol delete mode 100644 packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol delete mode 100644 packages/asset-swapper/contracts/src/KyberDmmSampler.sol delete mode 100644 packages/asset-swapper/contracts/src/UtilitySampler.sol delete mode 100644 packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts delete mode 100644 packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts diff --git a/packages/asset-swapper/contracts/src/CompoundSampler.sol b/packages/asset-swapper/contracts/src/CompoundSampler.sol deleted file mode 100644 index 2f68f59c7d..0000000000 --- a/packages/asset-swapper/contracts/src/CompoundSampler.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2021 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6; -pragma experimental ABIEncoderV2; - -import "./SamplerUtils.sol"; -import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; - -// Minimal CToken interface -interface ICToken { - function mint(uint mintAmount) external returns (uint); - function redeem(uint redeemTokens) external returns (uint); - function redeemUnderlying(uint redeemAmount) external returns (uint); - function exchangeRateStored() external view returns (uint); - function decimals() external view returns (uint8); -} - -contract CompoundSampler is SamplerUtils { - uint256 constant private EXCHANGE_RATE_SCALE = 1e10; - - function sampleSellsFromCompound( - ICToken cToken, - IERC20TokenV06 takerToken, - IERC20TokenV06 makerToken, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - // Exchange rate is scaled by 1 * 10^(18 - 8 + Underlying Token Decimals - uint256 exchangeRate = cToken.exchangeRateStored(); - uint256 cTokenDecimals = uint256(cToken.decimals()); - - if (address(makerToken) == address(cToken)) { - // mint - for (uint256 i = 0; i < numSamples; i++) { - makerTokenAmounts[i] = (takerTokenAmounts[i] * EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals) / exchangeRate; - } - - } else if (address(takerToken) == address(cToken)) { - // redeem - for (uint256 i = 0; i < numSamples; i++) { - makerTokenAmounts[i] = (takerTokenAmounts[i] * exchangeRate) / (EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals); - } - } - } - - function sampleBuysFromCompound( - ICToken cToken, - IERC20TokenV06 takerToken, - IERC20TokenV06 makerToken, - uint256[] memory makerTokenAmounts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - // Exchange rate is scaled by 1 * 10^(18 - 8 + Underlying Token Decimals - uint256 exchangeRate = cToken.exchangeRateStored(); - uint256 cTokenDecimals = uint256(cToken.decimals()); - - if (address(makerToken) == address(cToken)) { - // mint - for (uint256 i = 0; i < numSamples; i++) { - takerTokenAmounts[i] = makerTokenAmounts[i] * exchangeRate / (EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals); - } - } else if (address(takerToken) == address(cToken)) { - // redeem - for (uint256 i = 0; i < numSamples; i++) { - takerTokenAmounts[i] = (makerTokenAmounts[i] * EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals)/exchangeRate; - } - } - } -} diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol deleted file mode 100644 index 17493e2a46..0000000000 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6; -pragma experimental ABIEncoderV2; - -import "./BalancerSampler.sol"; -import "./BalancerV2Sampler.sol"; -import "./BancorSampler.sol"; -import "./CompoundSampler.sol"; -import "./CurveSampler.sol"; -import "./DODOSampler.sol"; -import "./DODOV2Sampler.sol"; -import "./KyberSampler.sol"; -import "./KyberDmmSampler.sol"; -import "./LidoSampler.sol"; -import "./LiquidityProviderSampler.sol"; -import "./MakerPSMSampler.sol"; -import "./MultiBridgeSampler.sol"; -import "./MStableSampler.sol"; -import "./MooniswapSampler.sol"; -import "./NativeOrderSampler.sol"; -import "./ShellSampler.sol"; -import "./SmoothySampler.sol"; -import "./TwoHopSampler.sol"; -import "./UniswapSampler.sol"; -import "./UniswapV2Sampler.sol"; -import "./UniswapV3Sampler.sol"; -import "./UtilitySampler.sol"; - - -contract ERC20BridgeSampler is - BalancerSampler, - BalancerV2Sampler, - BancorSampler, - CompoundSampler, - CurveSampler, - DODOSampler, - DODOV2Sampler, - KyberSampler, - KyberDmmSampler, - LidoSampler, - LiquidityProviderSampler, - MakerPSMSampler, - MStableSampler, - MooniswapSampler, - MultiBridgeSampler, - NativeOrderSampler, - ShellSampler, - SmoothySampler, - TwoHopSampler, - UniswapSampler, - UniswapV2Sampler, - UniswapV3Sampler, - UtilitySampler -{ - - struct CallResults { - bytes data; - bool success; - } - - /// @dev Call multiple public functions on this contract in a single transaction. - /// @param callDatas ABI-encoded call data for each function call. - /// @return callResults ABI-encoded results data for each call. - function batchCall(bytes[] calldata callDatas) - external - returns (CallResults[] memory callResults) - { - callResults = new CallResults[](callDatas.length); - for (uint256 i = 0; i != callDatas.length; ++i) { - callResults[i].success = true; - if (callDatas[i].length == 0) { - continue; - } - (callResults[i].success, callResults[i].data) = address(this).call(callDatas[i]); - } - } -} diff --git a/packages/asset-swapper/contracts/src/KyberDmmSampler.sol b/packages/asset-swapper/contracts/src/KyberDmmSampler.sol deleted file mode 100644 index 29ea8c01b3..0000000000 --- a/packages/asset-swapper/contracts/src/KyberDmmSampler.sol +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6; -pragma experimental ABIEncoderV2; - -interface IKyberDmmPool { - - function totalSupply() - external - view - returns (uint256); -} - -interface IKyberDmmFactory { - - function getPools(address token0, address token1) - external - view - returns (address[] memory _tokenPools); -} - -interface IKyberDmmRouter { - - function factory() external view returns (address); - - function getAmountsOut(uint256 amountIn, address[] calldata pools, address[] calldata path) - external - view - returns (uint256[] memory amounts); - - function getAmountsIn(uint256 amountOut, address[] calldata pools, address[] calldata path) - external - view - returns (uint256[] memory amounts); -} - - - -contract KyberDmmSampler -{ - /// @dev Gas limit for KyberDmm calls. - uint256 constant private KYBER_DMM_CALL_GAS = 150e3; // 150k - - /// @dev Sample sell quotes from KyberDmm. - /// @param router Router to look up tokens and amounts - /// @param path Token route. Should be takerToken -> makerToken - /// @param takerTokenAmounts Taker token sell amount for each sample. - /// @return pools The pool addresses involved in the multi path trade - /// @return makerTokenAmounts Maker amounts bought at each taker token - /// amount. - function sampleSellsFromKyberDmm( - address router, - address[] memory path, - uint256[] memory takerTokenAmounts - ) - public - view - returns (address[] memory pools, uint256[] memory makerTokenAmounts) - { - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - pools = _getKyberDmmPools(router, path); - if (pools.length == 0) { - return (pools, makerTokenAmounts); - } - for (uint256 i = 0; i < numSamples; i++) { - try - IKyberDmmRouter(router).getAmountsOut - {gas: KYBER_DMM_CALL_GAS} - (takerTokenAmounts[i], pools, path) - returns (uint256[] memory amounts) - { - makerTokenAmounts[i] = amounts[path.length - 1]; - // Break early if there are 0 amounts - if (makerTokenAmounts[i] == 0) { - break; - } - } catch (bytes memory) { - // Swallow failures, leaving all results as zero. - break; - } - } - } - - /// @dev Sample buy quotes from KyberDmm. - /// @param router Router to look up tokens and amounts - /// @param path Token route. Should be takerToken -> makerToken. - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return pools The pool addresses involved in the multi path trade - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromKyberDmm( - address router, - address[] memory path, - uint256[] memory makerTokenAmounts - ) - public - view - returns (address[] memory pools, uint256[] memory takerTokenAmounts) - { - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - pools = _getKyberDmmPools(router, path); - if (pools.length == 0) { - return (pools, takerTokenAmounts); - } - for (uint256 i = 0; i < numSamples; i++) { - try - IKyberDmmRouter(router).getAmountsIn - {gas: KYBER_DMM_CALL_GAS} - (makerTokenAmounts[i], pools, path) - returns (uint256[] memory amounts) - { - takerTokenAmounts[i] = amounts[0]; - // Break early if there are 0 amounts - if (takerTokenAmounts[i] == 0) { - break; - } - } catch (bytes memory) { - // Swallow failures, leaving all results as zero. - break; - } - } - } - - function _getKyberDmmPools( - address router, - address[] memory path - ) - private - view - returns (address[] memory pools) - { - IKyberDmmFactory factory = IKyberDmmFactory(IKyberDmmRouter(router).factory()); - pools = new address[](path.length - 1); - for (uint256 i = 0; i < pools.length; i++) { - // find the best pool - address[] memory allPools; - try - factory.getPools - {gas: KYBER_DMM_CALL_GAS} - (path[i], path[i + 1]) - returns (address[] memory allPools) - { - if (allPools.length == 0) { - return new address[](0); - } - - uint256 maxSupply = 0; - for (uint256 j = 0; j < allPools.length; j++) { - uint256 totalSupply = IKyberDmmPool(allPools[j]).totalSupply(); - if (totalSupply > maxSupply) { - maxSupply = totalSupply; - pools[i] = allPools[j]; - } - } - } catch (bytes memory) { - return new address[](0); - } - } - } -} diff --git a/packages/asset-swapper/contracts/src/UtilitySampler.sol b/packages/asset-swapper/contracts/src/UtilitySampler.sol deleted file mode 100644 index bbc3c5a1ad..0000000000 --- a/packages/asset-swapper/contracts/src/UtilitySampler.sol +++ /dev/null @@ -1,95 +0,0 @@ - -// SPDX-License-Identifier: Apache-2.0 -/* - - Copyright 2021 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6; -pragma experimental ABIEncoderV2; - -import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; - -contract UtilitySampler { - - using LibERC20TokenV06 for IERC20TokenV06; - - IERC20TokenV06 private immutable UTILITY_ETH_ADDRESS = IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); - - function getTokenDecimals(IERC20TokenV06[] memory tokens) - public - view - returns (uint256[] memory decimals) - { - decimals = new uint256[](tokens.length); - for (uint256 i = 0; i != tokens.length; i++) { - decimals[i] = tokens[i] == UTILITY_ETH_ADDRESS - ? 18 - : tokens[i].compatDecimals(); - } - } - - function getBalanceOf(IERC20TokenV06[] memory tokens, address account) - public - view - returns (uint256[] memory balances) - { - balances = new uint256[](tokens.length); - for (uint256 i = 0; i != tokens.length; i++) { - balances[i] = tokens[i] == UTILITY_ETH_ADDRESS - ? account.balance - : tokens[i].compatBalanceOf(account); - } - } - - function getAllowanceOf(IERC20TokenV06[] memory tokens, address account, address spender) - public - view - returns (uint256[] memory allowances) - { - allowances = new uint256[](tokens.length); - for (uint256 i = 0; i != tokens.length; i++) { - allowances[i] = tokens[i] == UTILITY_ETH_ADDRESS - ? 0 - : tokens[i].compatAllowance(account, spender); - } - } - - function isContract(address account) - public - view - returns (bool) - { - uint256 size; - assembly { size := extcodesize(account) } - return size > 0; - } - - function getGasLeft() - public - returns (uint256) - { - return gasleft(); - } - - function getBlockNumber() - public - view - returns (uint256) - { - return block.number; - } -} \ No newline at end of file diff --git a/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts b/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts deleted file mode 100644 index c56f12601e..0000000000 --- a/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BigNumber } from '@0x/utils'; - -import { ZERO_AMOUNT } from '../constants'; -export interface AaveInfo { - lendingPool: string; - aToken: string; - underlyingToken: string; -} -// tslint:disable-next-line:no-unnecessary-class -export class AaveV2Sampler { - public static sampleSellsFromAaveV2( - aaveInfo: AaveInfo, - takerToken: string, - makerToken: string, - takerTokenAmounts: BigNumber[], - ): BigNumber[] { - // Deposit/Withdrawal underlying <-> aToken is always 1:1 - if ( - (takerToken.toLowerCase() === aaveInfo.aToken.toLowerCase() && - makerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase()) || - (takerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase() && - makerToken.toLowerCase() === aaveInfo.aToken.toLowerCase()) - ) { - return takerTokenAmounts; - } - - // Not matching the reserve return 0 results - const numSamples = takerTokenAmounts.length; - - const makerTokenAmounts = new Array(numSamples); - makerTokenAmounts.fill(ZERO_AMOUNT); - return makerTokenAmounts; - } - - public static sampleBuysFromAaveV2( - aaveInfo: AaveInfo, - takerToken: string, - makerToken: string, - makerTokenAmounts: BigNumber[], - ): BigNumber[] { - // Deposit/Withdrawal underlying <-> aToken is always 1:1 - if ( - (takerToken.toLowerCase() === aaveInfo.aToken.toLowerCase() && - makerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase()) || - (takerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase() && - makerToken.toLowerCase() === aaveInfo.aToken.toLowerCase()) - ) { - return makerTokenAmounts; - } - - // Not matching the reserve return 0 results - const numSamples = makerTokenAmounts.length; - const takerTokenAmounts = new Array(numSamples); - takerTokenAmounts.fill(ZERO_AMOUNT); - return takerTokenAmounts; - } -} 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 4cea50d935..4ce2911d00 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 @@ -23,7 +23,6 @@ import { CalldataInfo, ExchangeProxyContractOpts, MarketBuySwapQuote, - MarketOperation, MarketSellSwapQuote, SwapQuote, SwapQuoteConsumerBase, @@ -39,7 +38,6 @@ import { SwapQuoteGenericBridgeOrder, SwapQuoteOrder, } from '../types'; -import { assert } from '../utils/assert'; import { valueByChainId } from '../utils/utils'; import { NATIVE_FEE_TOKEN_BY_CHAIN_ID, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index cb1e108e61..f26e14123e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -430,6 +430,7 @@ export const POLYGON_TOKENS = { WBTC: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', WMATIC: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', WETH: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', + nUSD: '0xb6c473756050de474286bed418b77aeac39b02af', }; export const AVALANCHE_TOKENS = { @@ -449,7 +450,6 @@ export const AVALANCHE_TOKENS = { aWETH: '0x53f7c5869a859f0aec3d334ee8b4cf01e3492f21', MIM: '0x130966628846bfd36ff31a822705796e8cb8c18d', DAI: '0xd586e7f844cea2f87f50152665bcbc2c279d8d70', - USDT: '0xc7198437980c041c805a1edcba50c1ce5db95118', }; export const CELO_TOKENS = { @@ -682,5 +682,4 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit dexSampleToReportSource(quote, side)); - // const multiHopSources = quotes.twoHopQuotes.map(quote => multiHopSampleToReportSource(quote, side)); - // const nativeSources = quotes.nativeOrders.map(order => - // nativeOrderToReportEntry( - // order.type, - // order as any, - // order.fillableTakerAmount, - // comparisonPrice, - // quoteRequestor, - // ), - // ); - // - // return { dexSources, multiHopSources, nativeSources }; + const { inputToken, outputToken, side, quotes } = marketSideLiquidity; + marketSideLiquidity.inputToken + const singleHopLiquidity = quotes + .filter(q => q.inputToken === inputToken && q.outputToken === outputToken) + .reduce((a, v) => ({ + ...a, + dexQuotes: [...a.dexQuotes, ...v.dexQuotes], + nativeOrders: [...a.nativeOrders, ...v.nativeOrders], + })); + const dexSources = singleHopLiquidity.dexQuotes.map(ss => ss.map(s => dexSampleToReportSource(side, s))).flat(2); + const multiHopSources = [] as MultiHopQuoteReportEntry[]; // TODO + const nativeSources = singleHopLiquidity.nativeOrders.map(order => + nativeOrderToReportEntry( + side, + order, + comparisonPrice, + quoteRequestor, + ), + ); + + return { dexSources, multiHopSources, nativeSources }; } constructor( @@ -143,40 +153,62 @@ export class MarketOperationUtils { public async getMarketSellLiquidityAsync( nativeOrders: SignedNativeOrder[], takerAmount: BigNumber, - opts?: Partial, + opts: GetMarketOrdersOpts, ): Promise { + if (nativeOrders.length === 0) { + throw new Error(AggregationError.EmptyOrders); + } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const { makerToken, takerToken } = nativeOrders[0].order; + nativeOrders = nativeOrders.filter(o => o.order.makerAmount.gt(0)); const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); const quoteSourceFilters = this._sellSources.merge(requestFilters); + const samplerSourceFilters = quoteSourceFilters.exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); + let sampleLegs: Address[][]; + let sampleAmounts: BigNumber[]; + { + const directLegs = this._getDirectSampleLegs(takerToken, makerToken); + const [multiHopLegs, multiHopAmounts] = quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) + ? await this._getMultiHopSampleLegsAndAmountsAsync({ + takerToken, + makerToken, + side: MarketOperation.Sell, + sources: samplerSourceFilters.sources, + inputAmount: takerAmount, + }) + : [[], []]; + sampleLegs = [...directLegs, ...multiHopLegs]; + sampleAmounts = [...directLegs.map(_ => takerAmount), ...multiHopAmounts]; + } + const terminalTokens = getTerminalTokensFromPaths(sampleLegs); + const [ tokenInfos, - [makerTokenToEthPrice, takerTokenToEthPrice], - dexQuotes, + tokenPricesPerEth, + samples, ] = await Promise.all([ this._sampler.getTokenInfosAsync( [makerToken, takerToken], ), this._sampler.getPricesAsync( - [ - [makerToken, this._nativeFeeToken], - [takerToken, this._nativeFeeToken], - ], + terminalTokens.map(t => [this._nativeFeeToken, t]), feeSourceFilters.sources, ), - this._sampler.getSellLiquidityAsync( - [makerToken, takerToken], - takerAmount, - quoteSourceFilters.sources, - ), + Promise.all(sampleLegs.map((hopPath, i) => + this._sampler.getSellLiquidityAsync( + hopPath, + sampleAmounts[i], + samplerSourceFilters.sources, + // Fetch fewer samples for multihop legs. + isDirectTokenPath(hopPath, makerToken, takerToken) ? undefined : 4, + ) + )), ]); - const [makerTokenInfo, takerTokenInfo] = tokenInfos; - const makerTokenDecimals = makerTokenInfo.decimals; - const takerTokenDecimals = takerTokenInfo.decimals; + const [{ decimals: makerTokenDecimals }, { decimals: takerTokenDecimals }] = tokenInfos; const isRfqSupported = !!_opts.rfqt; @@ -185,21 +217,161 @@ export class MarketOperationUtils { inputAmount: takerAmount, inputToken: takerToken, outputToken: makerToken, - outputAmountPerEth: makerTokenToEthPrice, - inputAmountPerEth: takerTokenToEthPrice, + tokenAmountPerEth: Object.assign( + {}, + ...terminalTokens.map((t, i) => ({ [t]: tokenPricesPerEth[i] })), + ), quoteSourceFilters, makerTokenDecimals: makerTokenDecimals, takerTokenDecimals: takerTokenDecimals, - quotes: { + gasPrice: opts.gasPrice, + quotes: sampleLegs.map((tokenPath, i) => ({ + tokenPath, + inputToken: tokenPath[0], + outputToken: tokenPath[tokenPath.length - 1], nativeOrders: [], - rfqtIndicativeQuotes: [], - // twoHopQuotes: [], - dexQuotes, - }, + dexQuotes: samples[i], + })).filter(doesRawHopQuotesHaveLiquidity), isRfqSupported, }; } + private async _getMultiHopSampleLegsAndAmountsAsync(opts: { + side: MarketOperation, + takerToken: Address, + makerToken: Address, + sources: ERC20BridgeSource[], + inputAmount: BigNumber, + hopAmountScaling?: number, + }): Promise<[Address[][], BigNumber[]]> { + const { + side, + takerToken, + makerToken, + sources, + inputAmount, + } = opts; + const hopAmountScaling = opts.hopAmountScaling === undefined ? 1.25 : opts.hopAmountScaling; + + const getIntermediateTokenPaths = (_takerToken: Address, _makerToken: Address, maxPathLength: number): Address[][] => { + if (maxPathLength < 2) { + return []; + } + const hopTokens = getIntermediateTokens( + _makerToken, + _takerToken, + DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID[this._sampler.chainId], + ); + const shortHops = hopTokens.map(t => [ + [_takerToken, t], + [t, _makerToken], + ]).flat(1); + // Find inner hops for each leg. + const deepHops = shortHops.map(([t, m]) => + getIntermediateTokenPaths(t, m, maxPathLength - 1) + .filter(innerPath => !innerPath.includes(_takerToken)) + .filter(innerPath => !innerPath.includes(_makerToken)) + .map(innerPath => innerPath[0] === t ? [...innerPath, m] : [t, ...innerPath]), + ).flat(1); + const paths = [ ...shortHops, ...deepHops ]; + // Prune duplicate paths. + return paths.filter((p, i) => !paths.find((o, j) => i < j && p.length === o.length && o.every((_v, k) => p[k] === o[k]))); + }; + const hopTokenPaths = getIntermediateTokenPaths(takerToken, makerToken, 3); + if (!hopTokenPaths.length) { + return [[],[]]; + } + const hopTokenPathPrices = await this._sampler.getPricesAsync( + hopTokenPaths, + sources, + ); + // Find eligible two-hops and compute their total price. + let twoHopPathDetails = hopTokenPaths.map((firstHop, firstHopIndex) => { + const firstHopPrice = hopTokenPathPrices[firstHopIndex]; + const [firstHopTakerToken, firstHopMakerToken] = getTakerMakerTokenFromTokenPath(firstHop); + if (firstHopTakerToken !== takerToken) { + return; + } + return hopTokenPaths.map((secondHop, secondHopIndex) => { + const secondHopPrice = hopTokenPathPrices[secondHopIndex]; + if (firstHop === secondHop) { + return; + } + const [secondHopTakerToken, secondHopMakerToken] = getTakerMakerTokenFromTokenPath(secondHop); + if (secondHopMakerToken !== makerToken) { + return; + } + if (firstHopMakerToken !== secondHopTakerToken) { + return; + } + const tokenPrices = [firstHopPrice, secondHopPrice]; + const totalPrice = tokenPrices.reduce((a, v) => a.times(v)); + return { + legs: [firstHop, secondHop], + tokenPrices, + totalPrice, + sampleAmounts: [] as BigNumber[], + }; + }); + }).flat(1).filter(v => !!v).map(v => v!); // TS hack to get around inferred undefined elements. + + // Sort two hops by descending total price and take the top 3. + twoHopPathDetails = twoHopPathDetails + .sort((a, b) => -a.totalPrice.comparedTo(b.totalPrice)) + .slice(0, 3); + + if (side === MarketOperation.Buy) { + // Reverse legs and prices and invert prices for buys. + for (const twoHop of twoHopPathDetails) { + twoHop.legs.reverse(); + twoHop.tokenPrices = twoHop.tokenPrices.map(p => new BigNumber(1).dividedBy(p)).reverse(); + } + } + // Compute the sample amount for each leg of each two hop. + for (const twoHop of twoHopPathDetails) { + const amounts = [inputAmount.integerValue()]; + for (let i = 0; i < twoHop.tokenPrices.length - 1; ++i) { + const lastAmount = amounts[amounts.length - 1]; + const prevPrice = twoHop.tokenPrices[i]; + amounts.push(lastAmount.times(prevPrice).times(hopAmountScaling).integerValue()); + } + twoHop.sampleAmounts = amounts; + } + // Flatten the legs of all two hops and remove duplicates. + const twoHopLegs = [] as Address[][]; + const twoHopSampleAmounts = [] as BigNumber[]; + for (const twoHop of twoHopPathDetails) { + for (const [hopLegIdx, legPath] of twoHop.legs.entries()) { + const sampleAmount = twoHop.sampleAmounts[hopLegIdx]; + const existingLegIdx = twoHopLegs.findIndex(existingLegPath => isSameTokenPath(legPath, existingLegPath)); + if (existingLegIdx !== -1) { + // We've already seen this leg/token path. Use the greater of + // the sample amounts. + twoHopSampleAmounts[existingLegIdx] = + BigNumber.max(twoHopSampleAmounts[existingLegIdx], sampleAmount); + } else { + twoHopLegs.push(legPath); + twoHopSampleAmounts.push(sampleAmount); + } + } + } + return [twoHopLegs, twoHopSampleAmounts]; + } + + private _getDirectSampleLegs( + takerToken: Address, + makerToken: Address, + ): Address[][] { + const hopTokens = getIntermediateTokens( + makerToken, + takerToken, + DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID[this._sampler.chainId], + ); + const directHop = [takerToken, makerToken]; + const hiddenHops = hopTokens.map(t => [takerToken, t, makerToken]); + return [ directHop, ...hiddenHops ]; + } + /** * Gets the liquidity available for a market buy operation * @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders @@ -210,96 +382,87 @@ export class MarketOperationUtils { public async getMarketBuyLiquidityAsync( nativeOrders: SignedNativeOrder[], makerAmount: BigNumber, - opts?: Partial, + opts: GetMarketOrdersOpts, ): Promise { - throw new Error(`Not implemented`); - // const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - // const { makerToken, takerToken } = nativeOrders[0].order; - // const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); - // - // const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); - // const quoteSourceFilters = this._buySources.merge(requestFilters); - // const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - // - // // Used to determine whether the tx origin is an EOA or a contract - // const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; - // - // // Call the sampler contract. - // const samplerPromise = this._sampler.executeAsync( - // this._sampler.getTokenDecimals([makerToken, takerToken]), - // // Get native order fillable amounts. - // this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), - // // Get ETH -> makerToken token price. - // this._sampler.getMedianSellRate( - // feeSourceFilters.sources, - // makerToken, - // this._nativeFeeToken, - // this._nativeFeeTokenAmount, - // ), - // // Get ETH -> taker token price. - // this._sampler.getMedianSellRate( - // feeSourceFilters.sources, - // takerToken, - // this._nativeFeeToken, - // this._nativeFeeTokenAmount, - // ), - // // Get buy quotes for taker -> maker. - // this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), - // this._sampler.getTwoHopBuyQuotes( - // quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [], - // makerToken, - // takerToken, - // makerAmount, - // ), - // this._sampler.isAddressContract(txOrigin), - // ); - // - // // Refresh the cached pools asynchronously if required - // void this._refreshPoolCacheIfRequiredAsync(takerToken, makerToken); - // - // const [ - // [ - // tokenDecimals, - // orderFillableMakerAmounts, - // ethToMakerAssetRate, - // ethToTakerAssetRate, - // dexQuotes, - // rawTwoHopQuotes, - // isTxOriginContract, - // ], - // ] = await Promise.all([samplerPromise]); - // - // // Filter out any invalid two hop quotes where we couldn't find a route - // const twoHopQuotes = rawTwoHopQuotes.filter( - // q => q && q.fillData && q.fillData.firstHopSource && q.fillData.secondHopSource, - // ); - // - // const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals; - // const isRfqSupported = !isTxOriginContract; - // - // const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({ - // ...order, - // ...getNativeAdjustedFillableAmountsFromMakerAmount(order, orderFillableMakerAmounts[i]), - // })); - // - // return { - // side: MarketOperation.Buy, - // inputAmount: makerAmount, - // inputToken: makerToken, - // outputToken: takerToken, - // outputAmountPerEth: ethToTakerAssetRate, - // inputAmountPerEth: ethToMakerAssetRate, - // quoteSourceFilters, - // makerTokenDecimals: makerTokenDecimals.toNumber(), - // takerTokenDecimals: takerTokenDecimals.toNumber(), - // quotes: { - // nativeOrders: limitOrdersWithFillableAmounts, - // rfqtIndicativeQuotes: [], - // twoHopQuotes, - // dexQuotes, - // }, - // isRfqSupported, - // }; + if (nativeOrders.length === 0) { + throw new Error(AggregationError.EmptyOrders); + } + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const { makerToken, takerToken } = nativeOrders[0].order; + nativeOrders = nativeOrders.filter(o => o.order.makerAmount.gt(0)); + + const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); + const quoteSourceFilters = this._buySources.merge(requestFilters); + const samplerSourceFilters = quoteSourceFilters.exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]); + const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); + + let sampleLegs: Address[][]; + let sampleAmounts: BigNumber[]; + { + const directLegs = this._getDirectSampleLegs(takerToken, makerToken); + const [multiHopLegs, multiHopAmounts] = quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) + ? await this._getMultiHopSampleLegsAndAmountsAsync({ + takerToken, + makerToken, + side: MarketOperation.Buy, + sources: samplerSourceFilters.sources, + inputAmount: makerAmount, + }) + : [[], []]; + sampleLegs = [...directLegs, ...multiHopLegs]; + sampleAmounts = [...directLegs.map(_ => makerAmount), ...multiHopAmounts]; + } + const terminalTokens = getTerminalTokensFromPaths(sampleLegs); + + const [ + tokenInfos, + tokenPricesPerEth, + samples, + ] = await Promise.all([ + this._sampler.getTokenInfosAsync( + [makerToken, takerToken], + ), + this._sampler.getPricesAsync( + terminalTokens.map(t => [this._nativeFeeToken, t]), + feeSourceFilters.sources, + ), + Promise.all(sampleLegs.map((hopPath, i) => + this._sampler.getBuyLiquidityAsync( + hopPath, + sampleAmounts[i], + samplerSourceFilters.sources, + // Fetch fewer samples for multihop legs. + isDirectTokenPath(hopPath, makerToken, takerToken) ? undefined : 4, + ) + )), + ]); + + const [{ decimals: makerTokenDecimals }, { decimals: takerTokenDecimals }] = tokenInfos; + + const isRfqSupported = !!_opts.rfqt; + + return { + side: MarketOperation.Buy, + inputAmount: makerAmount, + inputToken: makerToken, + outputToken: takerToken, + tokenAmountPerEth: Object.assign( + {}, + ...terminalTokens.map((t, i) => ({ [t]: tokenPricesPerEth[i] })), + ), + quoteSourceFilters, + makerTokenDecimals: makerTokenDecimals, + takerTokenDecimals: takerTokenDecimals, + gasPrice: opts.gasPrice, + quotes: sampleLegs.map((tokenPath, i) => ({ + tokenPath, + inputToken: tokenPath[tokenPath.length - 1], + outputToken: tokenPath[0], + nativeOrders: [], + dexQuotes: samples[i], + })).filter(doesRawHopQuotesHaveLiquidity), + isRfqSupported, + }; } /** @@ -316,101 +479,9 @@ export class MarketOperationUtils { public async getBatchMarketBuyOrdersAsync( batchNativeOrders: SignedNativeOrder[][], makerAmounts: BigNumber[], - opts: Partial & { gasPrice: BigNumber }, + opts: GetMarketOrdersOpts, ): Promise> { - throw new Error(`Not implemented`); - // if (batchNativeOrders.length === 0) { - // throw new Error(AggregationError.EmptyOrders); - // } - // const _opts: GetMarketOrdersOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - // - // const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources); - // const quoteSourceFilters = this._buySources.merge(requestFilters); - // - // const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - // - // const ops = [ - // ...batchNativeOrders.map(orders => - // this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy), - // ), - // ...batchNativeOrders.map(orders => - // this._sampler.getMedianSellRate( - // feeSourceFilters.sources, - // orders[0].order.takerToken, - // this._nativeFeeToken, - // this._nativeFeeTokenAmount, - // ), - // ), - // ...batchNativeOrders.map((orders, i) => - // this._sampler.getBuyQuotes( - // quoteSourceFilters.sources, - // orders[0].order.makerToken, - // orders[0].order.takerToken, - // [makerAmounts[i]], - // ), - // ), - // ...batchNativeOrders.map(orders => - // this._sampler.getTokenDecimals([orders[0].order.makerToken, orders[0].order.takerToken]), - // ), - // ]; - // - // const executeResults = await this._sampler.executeBatchAsync(ops); - // const batchOrderFillableMakerAmounts = 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 batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; - // const inputAmountPerEth = ZERO_AMOUNT; - // - // return Promise.all( - // batchNativeOrders.map(async (nativeOrders, i) => { - // if (nativeOrders.length === 0) { - // throw new Error(AggregationError.EmptyOrders); - // } - // const { makerToken, takerToken } = nativeOrders[0].order; - // const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; - // const outputAmountPerEth = batchEthToTakerAssetRate[i]; - // const dexQuotes = batchDexQuotes[i]; - // const makerAmount = makerAmounts[i]; - // try { - // const optimizerResult = await this._generateOptimizedOrdersAsync( - // { - // side: MarketOperation.Buy, - // inputToken: makerToken, - // outputToken: takerToken, - // inputAmount: makerAmount, - // outputAmountPerEth, - // inputAmountPerEth, - // quoteSourceFilters, - // makerTokenDecimals: batchTokenDecimals[i][0], - // takerTokenDecimals: batchTokenDecimals[i][1], - // quotes: { - // nativeOrders: nativeOrders.map((o, k) => ({ - // ...o, - // ...getNativeAdjustedFillableAmountsFromMakerAmount(o, orderFillableMakerAmounts[k]), - // })), - // dexQuotes, - // rfqtIndicativeQuotes: [], - // twoHopQuotes: [], - // }, - // isRfqSupported: false, - // }, - // { - // bridgeSlippage: _opts.bridgeSlippage, - // maxFallbackSlippage: _opts.maxFallbackSlippage, - // excludedSources: _opts.excludedSources, - // feeSchedule: _opts.feeSchedule, - // allowFallback: _opts.allowFallback, - // gasPrice: _opts.gasPrice, - // }, - // ); - // return optimizerResult; - // } catch (e) { - // // It's possible for one of the pairs to have no path - // // rather than throw NO_OPTIMAL_PATH we return undefined - // return undefined; - // } - // }), - // ); + throw new Error(`No implementado`); } public async _generateOptimizedOrdersAsync( @@ -418,129 +489,45 @@ export class MarketOperationUtils { opts: GenerateOptimizedOrdersOpts, ): Promise { const { - inputToken, - outputToken, side, inputAmount, + inputToken, + outputToken, quotes, - outputAmountPerEth, - inputAmountPerEth, + tokenAmountPerEth, } = marketSideLiquidity; - const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; - const orderOpts = { + const bestHopRoute = await this._findBestOptimizedHopRouteAsync( side, inputToken, outputToken, - orderDomain: this._orderDomain, - contractAddresses: this.contractAddresses, - bridgeSlippage: opts.bridgeSlippage || 0, - }; - - const augmentedRfqtIndicativeQuotes: NativeOrderWithFillableAmounts[] = rfqtIndicativeQuotes.map( - q => - // tslint:disable-next-line: no-object-literal-type-assertion - ({ - order: { ...new RfqOrder({ ...q }) }, - signature: INVALID_SIGNATURE, - fillableMakerAmount: new BigNumber(q.makerAmount), - fillableTakerAmount: new BigNumber(q.takerAmount), - fillableTakerFeeAmount: ZERO_AMOUNT, - type: FillQuoteTransformerOrderType.Rfq, - } as NativeOrderWithFillableAmounts), + inputAmount, + quotes, + { + tokenAmountPerEth, + exchangeProxyOverhead: opts.exchangeProxyOverhead, + slippage: opts.bridgeSlippage, + gasPrice: opts.gasPrice, + runLimit: opts.runLimit, + maxFallbackSlippage: opts.maxFallbackSlippage, + }, ); - - // Find the optimal path. - const penaltyOpts: PathPenaltyOpts = { - outputAmountPerEth, - inputAmountPerEth, - exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), - gasPrice: opts.gasPrice, - }; - - // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset - const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth; - const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth; - - let fills: Fill[][]; - // Find the optimal path using Rust router if enabled, otherwise fallback to JS Router - let optimalPath: Path | undefined; - if (SHOULD_USE_RUST_ROUTER) { - fills = [[]]; - optimalPath = findOptimalRustPathFromSamples( - side, - dexQuotes, - [...nativeOrders, ...augmentedRfqtIndicativeQuotes], - inputAmount, - penaltyOpts, - opts.feeSchedule, - this._sampler.chainId, - opts.neonRouterNumSamples, - opts.samplerMetrics, - ); - } else { - // Convert native orders and dex quotes into `Fill` objects. - fills = createFills({ - side, - orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], - dexQuotes, - targetInput: inputAmount, - outputAmountPerEth, - inputAmountPerEth, - excludedSources: opts.excludedSources, - feeSchedule: opts.feeSchedule, - }); - - optimalPath = await findOptimalPathJSAsync( - side, - fills, - inputAmount, - opts.runLimit, - opts.samplerMetrics, - penaltyOpts, - ); - } - - const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; - - const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( - marketSideLiquidity, - opts.feeSchedule, - opts.exchangeProxyOverhead, - ); - if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { - const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); - return { - optimizedOrders: twoHopOrders, - // liquidityDelivered: bestTwoHopQuote, - sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], - marketSideLiquidity, - adjustedRate: bestTwoHopRate, - takerAmountPerEth, - makerAmountPerEth, - }; - } - - // If there is no optimal path AND we didn't return a MultiHop quote, then throw - if (optimalPath === undefined) { + if (!bestHopRoute) { throw new Error(AggregationError.NoOptimalPath); } - // Generate a fallback path if required - // TODO(kimpers): Will experiment with disabling this and see how it affects revert rate - // to avoid yet another router roundtrip - // TODO: clean this up if we don't need it - // await this._addOptionalFallbackAsync(side, inputAmount, optimalPath, dexQuotes, fills, opts, penaltyOpts); - const collapsedPath = optimalPath.collapse(orderOpts); + // TODO: Find the unoptimized best rate to calculate savings from optimizer + const [takerToken, makerToken] = side === MarketOperation.Sell + ? [inputToken, outputToken] + : [outputToken, inputToken]; return { - optimizedOrders: collapsedPath.orders, + hops: bestHopRoute, + adjustedRate: getHopRouteOverallRate(bestHopRoute), // liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[], - sourceFlags: collapsedPath.sourceFlags, marketSideLiquidity, - adjustedRate: optimalPathRate, - takerAmountPerEth, - makerAmountPerEth, + takerAmountPerEth: tokenAmountPerEth[takerToken], + makerAmountPerEth: tokenAmountPerEth[makerToken], }; } @@ -558,12 +545,9 @@ export class MarketOperationUtils { bridgeSlippage: _opts.bridgeSlippage, maxFallbackSlippage: _opts.maxFallbackSlippage, excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, exchangeProxyOverhead: _opts.exchangeProxyOverhead, gasPrice: _opts.gasPrice, - neonRouterNumSamples: _opts.neonRouterNumSamples, - samplerMetrics: _opts.samplerMetrics, }; if (nativeOrders.length === 0) { @@ -596,8 +580,7 @@ export class MarketOperationUtils { optimizerResult.adjustedRate, amount, marketSideLiquidity, - _opts.feeSchedule, - _opts.exchangeProxyOverhead, + opts.gasPrice, ).wholeOrder; } @@ -630,7 +613,13 @@ export class MarketOperationUtils { }); // Re-run optimizer with the new indicative quote if (indicativeQuotes.length > 0) { - marketSideLiquidity.quotes.rfqtIndicativeQuotes = indicativeQuotes; + injectRfqLiquidity( + marketSideLiquidity.quotes, + side, + indicativeQuotes.map(indicativeRfqQuoteToSignedNativeOrder), + [], + RFQT_ORDER_GAS_COST, + ); optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); } } else { @@ -659,23 +648,7 @@ export class MarketOperationUtils { : await rfqt.firmQuoteValidator.getRfqtTakerFillableAmountsAsync( firmQuotes.map(q => new RfqOrder(q.order)), ); - - const quotesWithOrderFillableAmounts: NativeOrderWithFillableAmounts[] = firmQuotes.map( - (order, i) => ({ - ...order, - fillableTakerAmount: rfqTakerFillableAmounts[i], - // Adjust the maker amount by the available taker fill amount - fillableMakerAmount: getNativeAdjustedMakerFillAmount( - order.order, - rfqTakerFillableAmounts[i], - ), - fillableTakerFeeAmount: ZERO_AMOUNT, - }), - ); - marketSideLiquidity.quotes.nativeOrders = [ - ...quotesWithOrderFillableAmounts, - ...marketSideLiquidity.quotes.nativeOrders, - ]; + injectRfqLiquidity(marketSideLiquidity.quotes, side, firmQuotes, rfqTakerFillableAmounts, RFQT_ORDER_GAS_COST); // Re-run optimizer with the new firm quote. This is the second and last time // we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception @@ -694,23 +667,23 @@ export class MarketOperationUtils { // Compute Quote Report and return the results. let quoteReport: QuoteReport | undefined; if (_opts.shouldGenerateQuoteReport) { - quoteReport = MarketOperationUtils._computeQuoteReport( - _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, + quoteReport = MarketOperationUtils._computeQuoteReport({ + quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, + comparisonPrice: wholeOrderPrice, marketSideLiquidity, optimizerResult, - wholeOrderPrice, - ); + }); } // Always compute the Extended Quote Report let extendedQuoteReportSources: ExtendedQuoteReportSources | undefined; - extendedQuoteReportSources = MarketOperationUtils._computeExtendedQuoteReportSources( - _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, + extendedQuoteReportSources = MarketOperationUtils._computeExtendedQuoteReportSources({ + quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, + comparisonPrice: wholeOrderPrice, marketSideLiquidity, - amount, optimizerResult, - wholeOrderPrice, - ); + amount, + }); let priceComparisonsReport: PriceComparisonsReport | undefined; if (_opts.shouldIncludePriceComparisonsReport) { @@ -723,72 +696,365 @@ export class MarketOperationUtils { return { ...optimizerResult, quoteReport, extendedQuoteReportSources, priceComparisonsReport }; } - // tslint:disable-next-line: prefer-function-over-method - private async _addOptionalFallbackAsync( - side: MarketOperation, - inputAmount: BigNumber, - optimalPath: Path, - dexQuotes: DexSample[][], - fills: Fill[][], - opts: GenerateOptimizedOrdersOpts, - penaltyOpts: PathPenaltyOpts, - ): Promise { - const maxFallbackSlippage = opts.maxFallbackSlippage || 0; - const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; + private async _createOptimizedHopAsync(opts: { + side: MarketOperation; + outputAmountPerEth: BigNumber; + inputAmountPerEth: BigNumber; + inputToken: Address; + outputToken: Address; + inputAmount: BigNumber; + dexQuotes: DexSample[][]; + nativeOrders: NativeOrderWithFillableAmounts[]; + slippage: number; + gasPrice: BigNumber; + exchangeProxyOverhead: ExchangeProxyOverhead; + runLimit?: number; + maxFallbackSlippage: number; + }): Promise { + + let path = await this._findOptimalPathFromSamples({ + side: opts.side, + nativeOrders: opts.nativeOrders, + dexQuotes: opts.dexQuotes, + inputAmount: opts.inputAmount, + outputAmountPerEth: opts.outputAmountPerEth, + inputAmountPerEth: opts.inputAmountPerEth, + gasPrice: opts.gasPrice, + runLimit: opts.runLimit, + exchangeProxyOverhead: opts.exchangeProxyOverhead, + }); + // Convert native orders and dex quotes into `Fill` objects. + if (!path) { + return null; + } + + if (doesPathNeedFallback(path)) { + path = await this._addFallbackToPath({ + path, + side: opts.side, + dexQuotes: opts.dexQuotes, + inputAmount: opts.inputAmount, + outputAmountPerEth: opts.outputAmountPerEth, + inputAmountPerEth: opts.inputAmountPerEth, + gasPrice: opts.gasPrice, + runLimit: opts.runLimit, + maxFallbackSlippage: opts.maxFallbackSlippage, + exchangeProxyOverhead: opts.exchangeProxyOverhead, + }); + } + + const orders = path.collapse({ + side: opts.side, + inputToken: opts.inputToken, + outputToken: opts.outputToken, + }).orders; + + return { + orders, + inputToken: opts.inputToken, + outputToken: opts.outputToken, + inputAmount: path.size().input, + outputAmount: path.size().output, + adjustedCompleteRate: path.adjustedCompleteMakerToTakerRate(), + sourceFlags: path.sourceFlags, + }; + } + + private async _findOptimalPathFromSamples(opts: { + side: MarketOperation; + outputAmountPerEth: BigNumber; + inputAmountPerEth: BigNumber; + inputAmount: BigNumber; + dexQuotes: DexSample[][]; + nativeOrders: NativeOrderWithFillableAmounts[]; + gasPrice: BigNumber; + exchangeProxyOverhead: ExchangeProxyOverhead; + runLimit?: number; + }): Promise { + // Find the optimal path. + const penaltyOpts: PathPenaltyOpts = { + outputAmountPerEth: opts.outputAmountPerEth, + inputAmountPerEth: opts.inputAmountPerEth, + exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), + gasPrice: opts.gasPrice, + }; + + // Find the optimal path using Rust router if enabled, otherwise fallback to JS Router + if (SHOULD_USE_RUST_ROUTER) { + return findOptimalRustPathFromSamples( + opts.side, + opts.dexQuotes, + opts.nativeOrders, + opts.inputAmount, + penaltyOpts, + opts.gasPrice, + this._sampler.chainId, + ); + }; + + const fills = createFills({ + side: opts.side, + orders: opts.nativeOrders, + dexQuotes: opts.dexQuotes, + targetInput: opts.inputAmount, + outputAmountPerEth: opts.outputAmountPerEth, + inputAmountPerEth: opts.inputAmountPerEth, + gasPrice: opts.gasPrice, + }); + return findOptimalPathJSAsync(opts.side, fills, opts.inputAmount, opts.runLimit, penaltyOpts); + } + + private async _addFallbackToPath(opts: { + path: Path; + side: MarketOperation; + outputAmountPerEth: BigNumber; + inputAmountPerEth: BigNumber; + inputAmount: BigNumber; + dexQuotes: DexSample[][]; + gasPrice: BigNumber; + exchangeProxyOverhead: ExchangeProxyOverhead; + runLimit?: number; + maxFallbackSlippage: number; + }): Promise { + const { path } = opts; + const pathRate = path ? path.adjustedRate() : ZERO_AMOUNT; // Generate a fallback path if sources requiring a fallback (fragile) are in the optimal path. // Native is relatively fragile (limit order collision, expiry, or lack of available maker balance) // LiquidityProvider is relatively fragile (collision) const fragileSources = [ERC20BridgeSource.Native, ERC20BridgeSource.LiquidityProvider]; - const fragileFills = optimalPath.fills.filter(f => fragileSources.includes(f.source)); - if (opts.allowFallback && fragileFills.length !== 0) { - // We create a fallback path that is exclusive of Native liquidity - // This is the optimal on-chain path for the entire input amount - const sturdyPenaltyOpts = { - ...penaltyOpts, - exchangeProxyOverhead: (sourceFlags: bigint) => - // tslint:disable-next-line: no-bitwise - penaltyOpts.exchangeProxyOverhead(sourceFlags | optimalPath.sourceFlags), - }; + const fragileFills = path.fills.filter(f => fragileSources.includes(f.source)); + // We create a fallback path that is exclusive of Native liquidity + const sturdySamples = opts.dexQuotes + .filter(ss => ss.length > 0 && !fragileSources.includes(ss[0].source)); + // This is the optimal on-chain path for the entire input amount + let sturdyPath = await this._findOptimalPathFromSamples({ + side: opts.side, + nativeOrders: [], + dexQuotes: sturdySamples, + inputAmount: opts.inputAmount, + outputAmountPerEth: opts.outputAmountPerEth, + inputAmountPerEth: opts.inputAmountPerEth, + gasPrice: opts.gasPrice, + runLimit: opts.runLimit, + exchangeProxyOverhead: (sourceFlags: bigint) => + opts.exchangeProxyOverhead(sourceFlags | path.sourceFlags), + }); + // Calculate the slippage of on-chain sources compared to the most optimal path + // if within an acceptable threshold we enable a fallback to prevent reverts + if (sturdyPath && + (fragileFills.length === path.fills.length || + sturdyPath.adjustedSlippage(pathRate) <= opts.maxFallbackSlippage) + ) { + return Path.clone(path).addFallback(sturdyPath); + } + return path; + } - let sturdyOptimalPath: Path | undefined; - if (SHOULD_USE_RUST_ROUTER) { - const sturdySamples = dexQuotes.filter( - samples => samples.length > 0 && !fragileSources.includes(samples[0].source), - ); - sturdyOptimalPath = findOptimalRustPathFromSamples( - side, - sturdySamples, - [], - inputAmount, - sturdyPenaltyOpts, - opts.feeSchedule, - this._sampler.chainId, - opts.neonRouterNumSamples, - undefined, // hack: set sampler metrics to undefined to avoid fallback timings - ); - } else { - const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source)); - sturdyOptimalPath = await findOptimalPathJSAsync( - side, - sturdyFills, - inputAmount, - opts.runLimit, - undefined, // hack: set sampler metrics to undefined to avoid fallback timings - sturdyPenaltyOpts, - ); + // Find the and create the best sequence of OptimizedHops for a swap. + async _findBestOptimizedHopRouteAsync( + side: MarketOperation, + inputToken: Address, + outputToken: Address, + inputAmount: BigNumber, + hopQuotes: RawHopQuotes[], + opts: { + tokenAmountPerEth?: TokenAmountPerEth, + exchangeProxyOverhead?: ExchangeProxyOverhead, + slippage?: number, + gasPrice?: BigNumber, + runLimit?: number, + maxFallbackSlippage?: number, + } = {}, + ): Promise { + const findRoutes = (firstHop: RawHopQuotes, _hopQuotes: RawHopQuotes[] = hopQuotes): RawHopQuotes[][] => { + if (firstHop.inputToken === inputToken && firstHop.outputToken === outputToken) { + return [[firstHop]]; // Direct A -> B } - // Calculate the slippage of on-chain sources compared to the most optimal path - // if within an acceptable threshold we enable a fallback to prevent reverts - if ( - sturdyOptimalPath !== undefined && - (fragileFills.length === optimalPath.fills.length || - sturdyOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage) - ) { - optimalPath.addFallback(sturdyOptimalPath); + const otherHopQuotes = _hopQuotes.filter(h => h !== firstHop); + const r = []; + for (const h of otherHopQuotes) { + if (h.inputToken === firstHop.outputToken) { + if (h.outputToken === outputToken) { + r.push([firstHop, h]); + } else { + r.push(...findRoutes(h, otherHopQuotes).map(route => [firstHop, ...route])); + } + } + } + return r; + }; + const firstHops = hopQuotes.filter(h => h.inputToken === inputToken); + const routes = firstHops.map(firstHop => findRoutes(firstHop)).flat(1); + + const tokenAmountPerEth = opts.tokenAmountPerEth || {}; + const slippage = opts.slippage || 0; + const gasPrice = opts.gasPrice || ZERO_AMOUNT; + const runLimit = opts.runLimit; + const maxFallbackSlippage = opts.maxFallbackSlippage || 0; + const exchangeProxyOverhead = opts.exchangeProxyOverhead || (() => ZERO_AMOUNT); + const hopRoutes = (await Promise.all(routes.map(async route => { + let hopInputAmount = inputAmount; + const hops = []; + for (const routeHop of route) { + const hop = await this._createOptimizedHopAsync({ + side, + slippage, + gasPrice, + exchangeProxyOverhead, + runLimit, + maxFallbackSlippage, + inputAmount: hopInputAmount, + dexQuotes: routeHop.dexQuotes, + nativeOrders: routeHop.nativeOrders, + inputToken: routeHop.inputToken, + outputToken: routeHop.outputToken, + inputAmountPerEth: tokenAmountPerEth[routeHop.inputToken] || ZERO_AMOUNT, + outputAmountPerEth: tokenAmountPerEth[routeHop.outputToken] || ZERO_AMOUNT, + }); + if (!hop) { + // This hop could not satisfy the input amount so the + // whole route is invalid. + return []; + } + // Output of this hop will be the input for the next hop. + hopInputAmount = hop.outputAmount; + hops.push(hop); + } + return hops; + }))).filter(routes => routes.length); + if (hopRoutes.length === 0) { + return; + } + // Pick the route with the best rate. + let bestHopRoute; + let bestHopRouteTotalRate; + for (const route of hopRoutes) { + const rate = getHopRouteOverallRate(route); + if (!bestHopRouteTotalRate || rate.gt(bestHopRouteTotalRate)) { + bestHopRoute = route; + bestHopRouteTotalRate = rate; } } + return bestHopRoute; } */ } -// tslint:disable: max-file-line-count +function doesPathNeedFallback(path: Path): boolean { + const fragileSources = [ERC20BridgeSource.Native, ERC20BridgeSource.LiquidityProvider]; + return !!path.fills.find(f => fragileSources.includes(f.source)); +} + + +// Compute the overall adjusted rate for a multihop path. +function getHopRouteOverallRate(multiHopPaths: OptimizedHop[]): BigNumber { + return multiHopPaths.reduce( + (a, h) => a = a.times(h.adjustedCompleteRate), + new BigNumber(1), + ); +} + +function indicativeRfqQuoteToSignedNativeOrder(iq: V4RFQIndicativeQuote): SignedRfqOrder { + return { + order: { + chainId: 1, + verifyingContract: NULL_ADDRESS, + expiry: iq.expiry, + maker: NULL_ADDRESS, + taker: NULL_ADDRESS, + txOrigin: NULL_ADDRESS, + makerToken: iq.makerToken, + takerToken: iq.takerToken, + makerAmount: iq.makerAmount, + takerAmount: iq.takerAmount, + pool: '0x0', + salt: ZERO_AMOUNT, + }, + signature: { + r: '0x0', + s: '0x0', + v: 0, + signatureType: SignatureType.Invalid, + }, + type: FillQuoteTransformerOrderType.Rfq, + }; +} + +function injectRfqLiquidity( + quotes: RawHopQuotes[], + side: MarketOperation, + orders: SignedRfqOrder[], + orderFillableTakerAmounts: BigNumber[] = [], + gasCostPerOrder: number, +): void { + if (orders.length === 0) { + return; + } + const { makerToken, takerToken } = orders[0].order; + const fullOrders = orders.map((o, i) => ({ + ...o, + fillableTakerAmount: orderFillableTakerAmounts[i] || ZERO_AMOUNT, + fillableMakerAmount: getNativeOrderMakerFillAmount( + o.order, + orderFillableTakerAmounts[i], + ), + fillableTakerFeeAmount: ZERO_AMOUNT, + gasCost: gasCostPerOrder, + })); + const inputToken = side === MarketOperation.Sell ? takerToken : makerToken; + const outputToken = side === MarketOperation.Sell ? makerToken : takerToken; + // Insert into compatible hop quotes. + let wasInserted = false; + for (const q of quotes) { + if (q.inputToken === inputToken && q.outputToken === outputToken) { + q.nativeOrders.push(...fullOrders); + wasInserted = true; + } + } + // If there were no compatible hop quotes, create one. + if (!wasInserted) { + quotes.push({ + inputToken, + outputToken, + tokenPath: [takerToken, makerToken], + dexQuotes: [], + nativeOrders: fullOrders, + }); + } +} + +function getTakerMakerTokenFromTokenPath(tokenPath: Address[]): [Address, Address] { + return [tokenPath[0], tokenPath[tokenPath.length - 1]]; +} + +function getNativeOrderMakerFillAmount(order: CommonOrderFields, takerFillAmount: BigNumber): BigNumber { + // Round down because exchange rate favors Maker + return takerFillAmount + .multipliedBy(order.makerAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_DOWN); +} + +function getTerminalTokensFromPaths(paths: Address[][]): Address[] { + return [ + ...new Set( + paths + .map(leg => getTakerMakerTokenFromTokenPath(leg)) + .flat(1) + .map(t => t.toLowerCase()), + ), + ]; +} + +function doesRawHopQuotesHaveLiquidity(hopQuotes: RawHopQuotes): boolean { + return hopQuotes.dexQuotes.length > 0 || hopQuotes.nativeOrders.length > 0; +} + +function isSameTokenPath(a: Address[], b: Address[]): boolean { + return a.length === b.length && a.every((v, idx) => v === b[idx]); +} + + function isDirectTokenPath(tokenPath: Address[], makerToken: Address, takerToken: Address): boolean { + const [pathTakerToken, pathMakerToken] = getTakerMakerTokenFromTokenPath(tokenPath); + return pathTakerToken === takerToken && pathMakerToken === makerToken; + } 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 d53559a5be..d2b2230c5a 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 @@ -5,7 +5,6 @@ import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; import { performance } from 'perf_hooks'; -import { DEFAULT_INFO_LOGGER } from '../../constants'; import { NativeOrderWithFillableAmounts } from '../native_orders'; import { MarketOperation } from '../../types'; import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID } from '../market_operation_utils/constants'; @@ -14,7 +13,7 @@ import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID, ZERO_AMOUNT } from './constants'; import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills'; import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; import { getRate } from './rate_utils'; -import { DexSample, ERC20BridgeSource, Fill } from './types'; +import { DexSample, ERC20BridgeSource, Fill, SamplerMetrics } from './types'; // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise @@ -403,7 +402,6 @@ export function findOptimalRustPathFromSamples( nativeOrders, input, opts, - fees, neonRouterNumSamples, vipSourcesSet, ); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts deleted file mode 100644 index 8c4abd132a..0000000000 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BigNumber, logUtils, NULL_BYTES } from '@0x/utils'; - -import { ERC20BridgeSource, FillData, SourceQuoteOperation } from './types'; - -interface SamplerNoOperationCall { - callback: () => BigNumber[]; -} - -/** - * SamplerNoOperation can be used for sources where we already have all the necessary information - * required to perform the sample operations, without needing access to any on-chain data. Using a noop sample - * you can skip the eth_call, and just calculate the results directly in typescript land. - */ -export class SamplerNoOperation implements SourceQuoteOperation { - public readonly source: ERC20BridgeSource; - public fillData: TFillData; - private readonly _callback: () => BigNumber[]; - - constructor(opts: { source: ERC20BridgeSource; fillData?: TFillData } & SamplerNoOperationCall) { - this.source = opts.source; - this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion - this._callback = opts.callback; - } - - // tslint:disable-next-line:prefer-function-over-method - public encodeCall(): string { - return NULL_BYTES; - } - public handleCallResults(_callResults: string): BigNumber[] { - return this._callback(); - } - public handleRevert(_callResults: string): BigNumber[] { - logUtils.warn(`SamplerNoOperation: ${this.source} reverted`); - return []; - } -} 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 e363fcc9cc..a21ba5765f 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -1,7 +1,5 @@ import { - FillQuoteTransformerLimitOrderInfo, FillQuoteTransformerOrderType, - FillQuoteTransformerRfqOrderInfo, LimitOrderFields, RfqOrderFields, Signature, @@ -12,7 +10,7 @@ import { BigNumber } from '@0x/utils'; import { Address, Bytes, RfqFirmQuoteValidator, RfqRequestOpts } from '../../types'; import { NativeOrderWithFillableAmounts } from '../native_orders'; import { QuoteRequestor } from '../../utils/quote_requestor'; -import { PriceComparisonsReport, QuoteReport } from '../quote_report_generator'; +import { ExtendedQuoteReportSources, PriceComparisonsReport, QuoteReport } from '../quote_report_generator'; import { SourceFilters } from './source_filters'; @@ -429,6 +427,37 @@ export interface GetMarketOrdersOpts { * Gas price to use for quote */ gasPrice: BigNumber; + + /** + * Sampler metrics for recording data on the sampler service and operations + */ + samplerMetrics?: SamplerMetrics; +} + +export interface SamplerMetrics { + /** + * Logs the gas information performed during a sampler call. + * + * @param data.gasBefore The gas remaining measured before any operations have been performed + * @param data.gasAfter The gas remaining measured after all operations have been performed + */ + logGasDetails(data: { gasBefore: BigNumber; gasAfter: BigNumber }): void; + + /** + * Logs the block number + * + * @param blockNumber block number of the sampler call + */ + logBlockNumber(blockNumber: BigNumber): void; + + /** + * Logs the routing timings + * + * @param data.router The router type (neon-router or js) + * @param data.type The type of timing being recorded (e.g total timing, all sources timing or vip timing) + * @param data.timingMs The timing in milliseconds + */ + logRouterDetails(data: { router: 'neon-router' | 'js'; type: 'all' | 'vip' | 'total'; timingMs: number }): void; } /** @@ -461,6 +490,7 @@ export interface OptimizerResult { export interface OptimizerResultWithReport extends OptimizerResult { quoteReport?: QuoteReport; + extendedQuoteReportSources?: ExtendedQuoteReportSources; priceComparisonsReport?: PriceComparisonsReport; } diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts index 94a41080fd..806021d5c1 100644 --- a/packages/asset-swapper/src/utils/quote_report_generator.ts +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -11,7 +11,7 @@ import { OptimizedNativeOrder, } from './market_operation_utils/types'; import { NativeOrderWithFillableAmounts } from './native_orders'; -import { QuoteRequestor } from './quote_requestor'; +import { QuoteRequestor, V4RFQIndicativeQuoteMM } from './quote_requestor'; export interface QuoteReportEntryBase { liquiditySource: ERC20BridgeSource; @@ -179,6 +179,107 @@ export function generateQuoteReport(opts: { }; } +/** + * Generates a report of sources considered while computing the optimized + * swap quote, the sources ultimately included in the computed quote. This + * extende version incudes all considered quotes, not only native liquidity. + */ +export function generateExtendedQuoteReportSources( + marketOperation: MarketOperation, + inputToken: Address, + outputToken: Address, + rawHopQuotes: RawHopQuotes[], + hops: OptimizedHop[], + amount: BigNumber, + comparisonPrice?: BigNumber | undefined, + quoteRequestor?: QuoteRequestor, +): ExtendedQuoteReportSources { + const sourcesConsidered: ExtendedQuoteReportEntry[] = []; + + // NativeOrders + sourcesConsidered.push( + ...quotes.nativeOrders.map(order => + nativeOrderToReportEntry( + order.type, + order as any, + order.fillableTakerAmount, + comparisonPrice, + quoteRequestor, + ), + ), + ); + + // IndicativeQuotes + sourcesConsidered.push( + ...quotes.rfqtIndicativeQuotes.map(order => indicativeQuoteToReportEntry(order, comparisonPrice)), + ); + + // MultiHop + sourcesConsidered.push(...quotes.twoHopQuotes.map(quote => multiHopSampleToReportSource(quote, marketOperation))); + + // Dex Quotes + sourcesConsidered.push( + ..._.flatten( + quotes.dexQuotes.map(dex => + dex + .filter(quote => isDexSampleForTotalAmount(quote, marketOperation, amount)) + .map(quote => dexSampleToReportSource(quote, marketOperation)), + ), + ), + ); + const sourcesConsideredIndexed = sourcesConsidered.map( + (quote, index): ExtendedQuoteReportIndexedEntry => { + return { + ...quote, + quoteEntryIndex: index, + isDelivered: false, + }; + }, + ); + let sourcesDelivered; + if (Array.isArray(liquidityDelivered)) { + // create easy way to look up fillable amounts + const nativeOrderSignaturesToFillableAmounts = _.fromPairs( + quotes.nativeOrders.map(o => { + return [_nativeDataToId(o), o.fillableTakerAmount]; + }), + ); + // map sources delivered + sourcesDelivered = liquidityDelivered.map(collapsedFill => { + if (_isNativeOrderFromCollapsedFill(collapsedFill)) { + return nativeOrderToReportEntry( + collapsedFill.type, + collapsedFill.fillData, + nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)], + comparisonPrice, + quoteRequestor, + ); + } else { + return dexSampleToReportSource(collapsedFill, marketOperation); + } + }); + } else { + sourcesDelivered = [ + // tslint:disable-next-line: no-unnecessary-type-assertion + multiHopSampleToReportSource(liquidityDelivered as DexSample, marketOperation), + ]; + } + const sourcesDeliveredIndexed = sourcesDelivered.map( + (quote, index): ExtendedQuoteReportIndexedEntry => { + return { + ...quote, + quoteEntryIndex: index, + isDelivered: false, + }; + }, + ); + + return { + sourcesConsidered: sourcesConsideredIndexed, + sourcesDelivered: sourcesDeliveredIndexed, + }; +} + export function dexSampleToReportSource( side: MarketOperation, sample: DexSample, @@ -222,19 +323,19 @@ export function nativeOrderToReportEntry( signature = order.signature; [makerAmount, takerAmount] = [nativeOrder.makerAmount, nativeOrder.takerAmount]; } - const isRfqt = order.type === FillQuoteTransformerOrderType.Rfq; + const isRFQ = order.type === FillQuoteTransformerOrderType.Rfq; // if we find this is an rfqt order, label it as such and associate makerUri const rfqtMakerUri = - isRfqt && quoteRequestor ? quoteRequestor.getMakerUriForSignature(signature) : ''; + isRFQ && quoteRequestor ? quoteRequestor.getMakerUriForSignature(signature) : ''; return { makerAmount, takerAmount, - isRfqt, + isRFQ, fillableTakerAmount, liquiditySource: ERC20BridgeSource.Native, fillData: {}, - ...(isRfqt + ...(isRFQ ? { makerUri: rfqtMakerUri, nativeOrder, diff --git a/packages/asset-swapper/src/utils/utils.ts b/packages/asset-swapper/src/utils/utils.ts index ca37f2978c..fcc96811f3 100644 --- a/packages/asset-swapper/src/utils/utils.ts +++ b/packages/asset-swapper/src/utils/utils.ts @@ -14,6 +14,8 @@ export function valueByChainId(rest: Partial<{ [key in ChainId]: T }>, defaul [ChainId.PolygonMumbai]: defaultValue, [ChainId.Avalanche]: defaultValue, [ChainId.Fantom]: defaultValue, + [ChainId.Celo]: defaultValue, + [ChainId.Optimism]: defaultValue, ...(rest || {}), }; } diff --git a/yarn.lock b/yarn.lock index e7455a1e8e..c91de05d71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6525,9 +6525,9 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: readable-stream "^2.3.6" follow-redirects@^1.14.4: - version "1.14.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" - integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== follow-redirects@^1.12.1: version "1.14.9" @@ -13295,9 +13295,9 @@ ws@^5.1.1: async-limiter "~1.0.0" ws@^7.0.0: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== wsrun@^5.2.4: version "5.2.4"