diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json index e4fe0fe111..802e5f7022 100644 --- a/contracts/extensions/package.json +++ b/contracts/extensions/package.json @@ -11,6 +11,7 @@ }, "scripts": { "build": "yarn pre_build && tsc -b", + "build:ts": "tsc -b", "build:ci": "yarn build", "pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy", "test": "yarn run_mocha", diff --git a/contracts/integrations/test/framework/actors/maker.ts b/contracts/integrations/test/framework/actors/maker.ts index f242fff70e..59f3b9f51f 100644 --- a/contracts/integrations/test/framework/actors/maker.ts +++ b/contracts/integrations/test/framework/actors/maker.ts @@ -97,7 +97,7 @@ export function MakerMixin(Base: TBase): TBase & Cons const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize( this.actor.deployment.tokens.erc20, 4, // tslint:disable-line:custom-no-magic-numbers - ); + )!; // Maker and taker set balances/allowances to guarantee that the fill succeeds. // Amounts are chosen to be within each actor's balance (divided by 8, in case @@ -146,7 +146,7 @@ export function MakerMixin(Base: TBase): TBase & Cons const [leftMakerToken, leftTakerToken, makerFeeToken, takerFeeToken] = Pseudorandom.sampleSize( this.actor.deployment.tokens.erc20, 4, // tslint:disable-line:custom-no-magic-numbers - ); + )!; const rightMakerToken = leftTakerToken; const rightTakerToken = leftMakerToken; diff --git a/contracts/test-utils/src/test_with_reference.ts b/contracts/test-utils/src/test_with_reference.ts index fde39852e6..0cf332e6ac 100644 --- a/contracts/test-utils/src/test_with_reference.ts +++ b/contracts/test-utils/src/test_with_reference.ts @@ -144,7 +144,7 @@ function _getParameterNames(func: (...args: any[]) => any): string[] { .replace(/[/][/].*$/gm, '') // strip single-line comments .replace(/\s+/g, '') // strip white space .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments - .split('){', 1)[0] + .split(/\){|\)=>/, 1)[0] .replace(/^[^(]*[(]/, '') // extract the parameters .replace(/=[^,]+/g, '') // strip any ES6 defaults .split(',') diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 7d14917a1f..5908d3c6f7 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -61,6 +61,18 @@ { "note": "Added `Mooniswap`", "pr": 2675 + }, + { + "note": "Added two-hop support", + "pr": 2647 + }, + { + "note": "Move ERC20BridgeSampler interfaces into `interfaces` directory", + "pr": 2647 + }, + { + "note": "Use on-chain sampling (sometimes) for Balancer", + "pr": 2647 } ] }, diff --git a/packages/asset-swapper/contracts/src/BalancerSampler.sol b/packages/asset-swapper/contracts/src/BalancerSampler.sol new file mode 100644 index 0000000000..0950cc8e15 --- /dev/null +++ b/packages/asset-swapper/contracts/src/BalancerSampler.sol @@ -0,0 +1,143 @@ +/* + + Copyright 2019 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.5.9; +pragma experimental ABIEncoderV2; + +import "./interfaces/IBalancer.sol"; + + +contract BalancerSampler { + + /// @dev Base gas limit for Balancer calls. + uint256 constant private BALANCER_CALL_GAS = 300e3; // 300k + + struct BalancerState { + uint256 takerTokenBalance; + uint256 makerTokenBalance; + uint256 takerTokenWeight; + uint256 makerTokenWeight; + uint256 swapFee; + } + + /// @dev Sample sell quotes from Balancer. + /// @param poolAddress Address of the Balancer pool to query. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromBalancer( + address poolAddress, + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + IBalancer pool = IBalancer(poolAddress); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) { + return makerTokenAmounts; + } + + BalancerState memory poolState; + poolState.takerTokenBalance = pool.getBalance(takerToken); + poolState.makerTokenBalance = pool.getBalance(makerToken); + poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken); + poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken); + poolState.swapFee = pool.getSwapFee(); + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + poolAddress.staticcall.gas(BALANCER_CALL_GAS)( + abi.encodeWithSelector( + pool.calcOutGivenIn.selector, + poolState.takerTokenBalance, + poolState.takerTokenWeight, + poolState.makerTokenBalance, + poolState.makerTokenWeight, + takerTokenAmounts[i], + poolState.swapFee + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } else { + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + + /// @dev Sample buy quotes from Balancer. + /// @param poolAddress Address of the Balancer pool to query. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromBalancer( + address poolAddress, + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + IBalancer pool = IBalancer(poolAddress); + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) { + return takerTokenAmounts; + } + + BalancerState memory poolState; + poolState.takerTokenBalance = pool.getBalance(takerToken); + poolState.makerTokenBalance = pool.getBalance(makerToken); + poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken); + poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken); + poolState.swapFee = pool.getSwapFee(); + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + poolAddress.staticcall.gas(BALANCER_CALL_GAS)( + abi.encodeWithSelector( + pool.calcInGivenOut.selector, + poolState.takerTokenBalance, + poolState.takerTokenWeight, + poolState.makerTokenBalance, + poolState.makerTokenWeight, + makerTokenAmounts[i], + poolState.swapFee + )); + uint256 sellAmount = 0; + if (didSucceed) { + sellAmount = abi.decode(resultData, (uint256)); + } else { + break; + } + takerTokenAmounts[i] = sellAmount; + } + } +} diff --git a/packages/asset-swapper/contracts/src/CurveSampler.sol b/packages/asset-swapper/contracts/src/CurveSampler.sol index e31279817c..62e4d611e0 100644 --- a/packages/asset-swapper/contracts/src/CurveSampler.sol +++ b/packages/asset-swapper/contracts/src/CurveSampler.sol @@ -19,7 +19,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./ICurve.sol"; +import "./interfaces/ICurve.sol"; import "./ApproximateBuys.sol"; import "./SamplerUtils.sol"; diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol index 0b634b9ba3..6e75013052 100644 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol @@ -19,6 +19,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "./BalancerSampler.sol"; import "./CurveSampler.sol"; import "./Eth2DaiSampler.sol"; import "./KyberSampler.sol"; @@ -29,9 +30,11 @@ import "./MooniswapSampler.sol"; import "./NativeOrderSampler.sol"; import "./UniswapSampler.sol"; import "./UniswapV2Sampler.sol"; +import "./TwoHopSampler.sol"; contract ERC20BridgeSampler is + BalancerSampler, CurveSampler, Eth2DaiSampler, KyberSampler, @@ -40,6 +43,7 @@ contract ERC20BridgeSampler is MooniswapSampler, MultiBridgeSampler, NativeOrderSampler, + TwoHopSampler, UniswapSampler, UniswapV2Sampler { diff --git a/packages/asset-swapper/contracts/src/Eth2DaiSampler.sol b/packages/asset-swapper/contracts/src/Eth2DaiSampler.sol index 977508be66..216cdfc707 100644 --- a/packages/asset-swapper/contracts/src/Eth2DaiSampler.sol +++ b/packages/asset-swapper/contracts/src/Eth2DaiSampler.sol @@ -20,7 +20,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; -import "./IEth2Dai.sol"; +import "./interfaces/IEth2Dai.sol"; import "./SamplerUtils.sol"; diff --git a/packages/asset-swapper/contracts/src/KyberSampler.sol b/packages/asset-swapper/contracts/src/KyberSampler.sol index 59fca06978..67c6466049 100644 --- a/packages/asset-swapper/contracts/src/KyberSampler.sol +++ b/packages/asset-swapper/contracts/src/KyberSampler.sol @@ -20,10 +20,10 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; -import "./IKyberNetwork.sol"; -import "./IKyberNetworkProxy.sol"; -import "./IKyberStorage.sol"; -import "./IKyberHintHandler.sol"; +import "./interfaces/IKyberNetwork.sol"; +import "./interfaces/IKyberNetworkProxy.sol"; +import "./interfaces/IKyberStorage.sol"; +import "./interfaces/IKyberHintHandler.sol"; import "./ApproximateBuys.sol"; import "./SamplerUtils.sol"; diff --git a/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol b/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol index 3acbb17795..0c74b521d7 100644 --- a/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol +++ b/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol @@ -20,8 +20,8 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; -import "./ILiquidityProvider.sol"; -import "./ILiquidityProviderRegistry.sol"; +import "./interfaces/ILiquidityProvider.sol"; +import "./interfaces/ILiquidityProviderRegistry.sol"; import "./ApproximateBuys.sol"; import "./SamplerUtils.sol"; @@ -48,21 +48,21 @@ contract LiquidityProviderSampler is ) public view - returns (uint256[] memory makerTokenAmounts) + returns (uint256[] memory makerTokenAmounts, address providerAddress) { // Initialize array of maker token amounts. uint256 numSamples = takerTokenAmounts.length; makerTokenAmounts = new uint256[](numSamples); // Query registry for provider address. - address providerAddress = getLiquidityProviderFromRegistry( + providerAddress = _getLiquidityProviderFromRegistry( registryAddress, takerToken, makerToken ); // If provider doesn't exist, return all zeros. if (providerAddress == address(0)) { - return makerTokenAmounts; + return (makerTokenAmounts, providerAddress); } for (uint256 i = 0; i < numSamples; i++) { @@ -101,9 +101,14 @@ contract LiquidityProviderSampler is ) public view - returns (uint256[] memory takerTokenAmounts) + returns (uint256[] memory takerTokenAmounts, address providerAddress) { - return _sampleApproximateBuys( + providerAddress = _getLiquidityProviderFromRegistry( + registryAddress, + takerToken, + makerToken + ); + takerTokenAmounts = _sampleApproximateBuys( ApproximateBuyQuoteOpts({ makerTokenData: abi.encode(makerToken, registryAddress), takerTokenData: abi.encode(takerToken, registryAddress), @@ -119,12 +124,12 @@ contract LiquidityProviderSampler is /// @param takerToken Taker asset managed by liquidity provider. /// @param makerToken Maker asset managed by liquidity provider. /// @return providerAddress Address of the liquidity provider. - function getLiquidityProviderFromRegistry( + function _getLiquidityProviderFromRegistry( address registryAddress, address takerToken, address makerToken ) - public + private view returns (address providerAddress) { @@ -167,6 +172,7 @@ contract LiquidityProviderSampler is return 0; } // solhint-disable-next-line indent - return abi.decode(resultData, (uint256[]))[0]; + (uint256[] memory amounts, ) = abi.decode(resultData, (uint256[], address)); + return amounts[0]; } } diff --git a/packages/asset-swapper/contracts/src/MStableSampler.sol b/packages/asset-swapper/contracts/src/MStableSampler.sol index fa468824a6..205f86b91d 100644 --- a/packages/asset-swapper/contracts/src/MStableSampler.sol +++ b/packages/asset-swapper/contracts/src/MStableSampler.sol @@ -21,7 +21,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; -import "./IMStable.sol"; +import "./interfaces/IMStable.sol"; import "./ApproximateBuys.sol"; import "./SamplerUtils.sol"; diff --git a/packages/asset-swapper/contracts/src/MultiBridgeSampler.sol b/packages/asset-swapper/contracts/src/MultiBridgeSampler.sol index 59ca4a7c98..2be713dd8b 100644 --- a/packages/asset-swapper/contracts/src/MultiBridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/MultiBridgeSampler.sol @@ -19,7 +19,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./IMultiBridge.sol"; +import "./interfaces/IMultiBridge.sol"; contract MultiBridgeSampler { diff --git a/packages/asset-swapper/contracts/src/TwoHopSampler.sol b/packages/asset-swapper/contracts/src/TwoHopSampler.sol new file mode 100644 index 0000000000..9f03a2f2f7 --- /dev/null +++ b/packages/asset-swapper/contracts/src/TwoHopSampler.sol @@ -0,0 +1,125 @@ +/* + + Copyright 2019 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.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; + + +contract TwoHopSampler { + using LibBytes for bytes; + + struct HopInfo { + uint256 sourceIndex; + bytes returnData; + } + + function sampleTwoHopSell( + bytes[] memory firstHopCalls, + bytes[] memory secondHopCalls, + uint256 sellAmount + ) + public + view + returns ( + HopInfo memory firstHop, + HopInfo memory secondHop, + uint256 buyAmount + ) + { + uint256 intermediateAssetAmount = 0; + for (uint256 i = 0; i != firstHopCalls.length; ++i) { + firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, sellAmount); + (bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]); + if (didSucceed) { + uint256 amount = returnData.readUint256(returnData.length - 32); + if (amount > intermediateAssetAmount) { + intermediateAssetAmount = amount; + firstHop.sourceIndex = i; + firstHop.returnData = returnData; + } + } + } + if (intermediateAssetAmount == 0) { + return (firstHop, secondHop, buyAmount); + } + for (uint256 j = 0; j != secondHopCalls.length; ++j) { + secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, intermediateAssetAmount); + (bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]); + if (didSucceed) { + uint256 amount = returnData.readUint256(returnData.length - 32); + if (amount > buyAmount) { + buyAmount = amount; + secondHop.sourceIndex = j; + secondHop.returnData = returnData; + } + } + } + } + + function sampleTwoHopBuy( + bytes[] memory firstHopCalls, + bytes[] memory secondHopCalls, + uint256 buyAmount + ) + public + view + returns ( + HopInfo memory firstHop, + HopInfo memory secondHop, + uint256 sellAmount + ) + { + sellAmount = uint256(-1); + uint256 intermediateAssetAmount = uint256(-1); + for (uint256 j = 0; j != secondHopCalls.length; ++j) { + secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, buyAmount); + (bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]); + if (didSucceed) { + uint256 amount = returnData.readUint256(returnData.length - 32); + if ( + amount > 0 && + amount < intermediateAssetAmount + ) { + intermediateAssetAmount = amount; + secondHop.sourceIndex = j; + secondHop.returnData = returnData; + } + } + } + if (intermediateAssetAmount == uint256(-1)) { + return (firstHop, secondHop, sellAmount); + } + for (uint256 i = 0; i != firstHopCalls.length; ++i) { + firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, intermediateAssetAmount); + (bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]); + if (didSucceed) { + uint256 amount = returnData.readUint256(returnData.length - 32); + if ( + amount > 0 && + amount < sellAmount + ) { + sellAmount = amount; + firstHop.sourceIndex = i; + firstHop.returnData = returnData; + } + } + } + } +} diff --git a/packages/asset-swapper/contracts/src/UniswapSampler.sol b/packages/asset-swapper/contracts/src/UniswapSampler.sol index 7137653daa..313512ef5f 100644 --- a/packages/asset-swapper/contracts/src/UniswapSampler.sol +++ b/packages/asset-swapper/contracts/src/UniswapSampler.sol @@ -25,7 +25,7 @@ import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; -import "./IUniswapExchangeQuotes.sol"; +import "./interfaces/IUniswapExchangeQuotes.sol"; import "./SamplerUtils.sol"; diff --git a/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol b/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol index 09c0963cc3..5ae117c458 100644 --- a/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol +++ b/packages/asset-swapper/contracts/src/UniswapV2Sampler.sol @@ -20,7 +20,7 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; -import "./IUniswapV2Router01.sol"; +import "./interfaces/IUniswapV2Router01.sol"; contract UniswapV2Sampler is diff --git a/packages/asset-swapper/contracts/src/interfaces/IBalancer.sol b/packages/asset-swapper/contracts/src/interfaces/IBalancer.sol new file mode 100644 index 0000000000..78082fb88a --- /dev/null +++ b/packages/asset-swapper/contracts/src/interfaces/IBalancer.sol @@ -0,0 +1,43 @@ +/* + + Copyright 2019 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.5.9; + + +interface IBalancer { + function isBound(address t) external view returns (bool); + function getDenormalizedWeight(address token) external view returns (uint256); + function getBalance(address token) external view returns (uint256); + function getSwapFee() external view returns (uint256); + function calcOutGivenIn( + uint256 tokenBalanceIn, + uint256 tokenWeightIn, + uint256 tokenBalanceOut, + uint256 tokenWeightOut, + uint256 tokenAmountIn, + uint256 swapFee + ) external pure returns (uint256 tokenAmountOut); + function calcInGivenOut( + uint256 tokenBalanceIn, + uint256 tokenWeightIn, + uint256 tokenBalanceOut, + uint256 tokenWeightOut, + uint256 tokenAmountOut, + uint256 swapFee + ) external pure returns (uint256 tokenAmountIn); +} diff --git a/packages/asset-swapper/contracts/src/ICurve.sol b/packages/asset-swapper/contracts/src/interfaces/ICurve.sol similarity index 100% rename from packages/asset-swapper/contracts/src/ICurve.sol rename to packages/asset-swapper/contracts/src/interfaces/ICurve.sol diff --git a/packages/asset-swapper/contracts/src/IEth2Dai.sol b/packages/asset-swapper/contracts/src/interfaces/IEth2Dai.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IEth2Dai.sol rename to packages/asset-swapper/contracts/src/interfaces/IEth2Dai.sol diff --git a/packages/asset-swapper/contracts/src/IKyberHintHandler.sol b/packages/asset-swapper/contracts/src/interfaces/IKyberHintHandler.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IKyberHintHandler.sol rename to packages/asset-swapper/contracts/src/interfaces/IKyberHintHandler.sol diff --git a/packages/asset-swapper/contracts/src/IKyberNetwork.sol b/packages/asset-swapper/contracts/src/interfaces/IKyberNetwork.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IKyberNetwork.sol rename to packages/asset-swapper/contracts/src/interfaces/IKyberNetwork.sol diff --git a/packages/asset-swapper/contracts/src/IKyberNetworkProxy.sol b/packages/asset-swapper/contracts/src/interfaces/IKyberNetworkProxy.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IKyberNetworkProxy.sol rename to packages/asset-swapper/contracts/src/interfaces/IKyberNetworkProxy.sol diff --git a/packages/asset-swapper/contracts/src/IKyberStorage.sol b/packages/asset-swapper/contracts/src/interfaces/IKyberStorage.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IKyberStorage.sol rename to packages/asset-swapper/contracts/src/interfaces/IKyberStorage.sol diff --git a/packages/asset-swapper/contracts/src/ILiquidityProvider.sol b/packages/asset-swapper/contracts/src/interfaces/ILiquidityProvider.sol similarity index 100% rename from packages/asset-swapper/contracts/src/ILiquidityProvider.sol rename to packages/asset-swapper/contracts/src/interfaces/ILiquidityProvider.sol diff --git a/packages/asset-swapper/contracts/src/ILiquidityProviderRegistry.sol b/packages/asset-swapper/contracts/src/interfaces/ILiquidityProviderRegistry.sol similarity index 100% rename from packages/asset-swapper/contracts/src/ILiquidityProviderRegistry.sol rename to packages/asset-swapper/contracts/src/interfaces/ILiquidityProviderRegistry.sol diff --git a/packages/asset-swapper/contracts/src/IMStable.sol b/packages/asset-swapper/contracts/src/interfaces/IMStable.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IMStable.sol rename to packages/asset-swapper/contracts/src/interfaces/IMStable.sol diff --git a/packages/asset-swapper/contracts/src/IMultiBridge.sol b/packages/asset-swapper/contracts/src/interfaces/IMultiBridge.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IMultiBridge.sol rename to packages/asset-swapper/contracts/src/interfaces/IMultiBridge.sol diff --git a/packages/asset-swapper/contracts/src/IUniswapExchangeQuotes.sol b/packages/asset-swapper/contracts/src/interfaces/IUniswapExchangeQuotes.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IUniswapExchangeQuotes.sol rename to packages/asset-swapper/contracts/src/interfaces/IUniswapExchangeQuotes.sol diff --git a/packages/asset-swapper/contracts/src/IUniswapV2Router01.sol b/packages/asset-swapper/contracts/src/interfaces/IUniswapV2Router01.sol similarity index 100% rename from packages/asset-swapper/contracts/src/IUniswapV2Router01.sol rename to packages/asset-swapper/contracts/src/interfaces/IUniswapV2Router01.sol diff --git a/packages/asset-swapper/contracts/src/DummyLiquidityProvider.sol b/packages/asset-swapper/contracts/test/DummyLiquidityProvider.sol similarity index 100% rename from packages/asset-swapper/contracts/src/DummyLiquidityProvider.sol rename to packages/asset-swapper/contracts/test/DummyLiquidityProvider.sol diff --git a/packages/asset-swapper/contracts/src/DummyLiquidityProviderRegistry.sol b/packages/asset-swapper/contracts/test/DummyLiquidityProviderRegistry.sol similarity index 100% rename from packages/asset-swapper/contracts/src/DummyLiquidityProviderRegistry.sol rename to packages/asset-swapper/contracts/test/DummyLiquidityProviderRegistry.sol diff --git a/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol b/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol index 64b58411f3..2d70c54124 100644 --- a/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/test/TestERC20BridgeSampler.sol @@ -22,9 +22,9 @@ import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFacto import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "../src/ERC20BridgeSampler.sol"; -import "../src/IEth2Dai.sol"; -import "../src/IKyberNetworkProxy.sol"; -import "../src/IUniswapV2Router01.sol"; +import "../src/interfaces/IEth2Dai.sol"; +import "../src/interfaces/IKyberNetworkProxy.sol"; +import "../src/interfaces/IUniswapV2Router01.sol"; library LibDeterministicQuotes { diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 39949fe3c9..2568bc65df 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -9,6 +9,7 @@ "types": "lib/src/index.d.ts", "scripts": { "build": "yarn pre_build && tsc -b", + "build:ts": "tsc -b", "watch": "tsc -w -p tsconfig.json", "watch:contracts": "sol-compiler -w", "build:ci": "yarn build", @@ -37,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ApproximateBuys|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|UniswapSampler|UniswapV2Sampler).json", + "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", "postpublish": { "assets": [] } @@ -60,6 +61,7 @@ "@0x/base-contract": "^6.2.3", "@0x/contract-addresses": "^4.11.0", "@0x/contract-wrappers": "^13.8.0", + "@0x/contracts-erc20-bridge-sampler": "^1.7.0", "@0x/json-schemas": "^5.1.0", "@0x/order-utils": "^10.3.0", "@0x/orderbook": "^2.2.7", diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index d24ff0154a..54a0b0d4a0 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -39,10 +39,8 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000); const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { - ...{ - chainId: MAINNET_CHAIN_ID, - orderRefreshIntervalMs: 10000, // 10 seconds - }, + chainId: MAINNET_CHAIN_ID, + orderRefreshIntervalMs: 10000, // 10 seconds ...DEFAULT_ORDER_PRUNER_OPTS, samplerGasLimit: 250e6, ethGasStationUrl: ETH_GAS_STATION_API_URL, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 0d259c7cd9..0d5701c4c2 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -1,3 +1,9 @@ +export { + AwaitTransactionSuccessOpts, + ContractFunctionObj, + ContractTxFunctionObj, + SendTransactionOpts, +} from '@0x/base-contract'; export { ContractAddresses } from '@0x/contract-addresses'; export { WSOpts } from '@0x/mesh-rpc-client'; export { @@ -28,6 +34,7 @@ export { AbiDefinition, BlockParam, BlockParamLiteral, + CallData, CompilerOpts, CompilerSettings, CompilerSettingsMetadata, @@ -66,6 +73,8 @@ export { StateMutability, SupportedProvider, TupleDataItem, + TxData, + TxDataPayable, Web3JsProvider, Web3JsV1Provider, Web3JsV2Provider, @@ -107,6 +116,11 @@ export { SwapQuoterRfqtOpts, } from './types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; +export { + Parameters, + SamplerContractCall, + SamplerContractOperation, +} from './utils/market_operation_utils/sampler_contract_operation'; export { BancorFillData, BalancerFillData, @@ -114,22 +128,30 @@ export { CurveFillData, CurveFunctionSelectors, CurveInfo, + DexSample, ERC20BridgeSource, FeeSchedule, + Fill, FillData, + FillFlags, GetMarketOrdersRfqtOpts, LiquidityProviderFillData, MarketDepth, MarketDepthSide, MultiBridgeFillData, + MultiHopFillData, NativeCollapsedFill, NativeFillData, OptimizedMarketOrder, + SourceInfo, + SourceQuoteOperation, + TokenAdjacencyGraph, UniswapV2FillData, } from './utils/market_operation_utils/types'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { BridgeReportSource, + MultiHopReportSource, NativeOrderbookReportSource, NativeRFQTReportSource, QuoteReport, @@ -140,3 +162,4 @@ export { rfqtMocker } from './utils/rfqt_mocker'; export { ERC20BridgeSamplerContract } from './wrappers'; import { ERC20BridgeSource } from './utils/market_operation_utils/types'; export type Native = ERC20BridgeSource.Native; +export type MultiHop = ERC20BridgeSource.MultiHop; 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 5fb2c44f63..b77dffac6e 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 @@ -1,16 +1,13 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { ITransformERC20Contract } from '@0x/contract-wrappers'; import { - assetDataUtils, encodeAffiliateFeeTransformerData, encodeFillQuoteTransformerData, encodePayTakerTransformerData, encodeWethTransformerData, - ERC20AssetData, ETH_TOKEN_ADDRESS, FillQuoteTransformerSide, } from '@0x/order-utils'; -import { AssetProxyId } from '@0x/types'; import { BigNumber, providerUtils } from '@0x/utils'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import * as ethjs from 'ethereumjs-util'; @@ -30,6 +27,7 @@ import { SwapQuoteGetOutputOpts, } from '../types'; import { assert } from '../utils/assert'; +import { getTokenFromAssetData } from '../utils/utils'; // tslint:disable-next-line:custom-no-magic-numbers const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); @@ -108,19 +106,48 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }); } + const intermediateToken = quote.isTwoHop ? getTokenFromAssetData(quote.orders[0].makerAssetData) : NULL_ADDRESS; // This transformer will fill the quote. - transforms.push({ - deploymentNonce: this.transformerNonces.fillQuoteTransformer, - data: encodeFillQuoteTransformerData({ - sellToken, - buyToken, - side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, - fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount, - maxOrderFillAmounts: [], - orders: quote.orders, - signatures: quote.orders.map(o => o.signature), - }), - }); + if (quote.isTwoHop) { + const [firstHopOrder, secondHopOrder] = quote.orders; + transforms.push({ + deploymentNonce: this.transformerNonces.fillQuoteTransformer, + data: encodeFillQuoteTransformerData({ + sellToken, + buyToken: intermediateToken, + side: FillQuoteTransformerSide.Sell, + fillAmount: firstHopOrder.takerAssetAmount, + maxOrderFillAmounts: [], + orders: [firstHopOrder], + signatures: [firstHopOrder.signature], + }), + }); + transforms.push({ + deploymentNonce: this.transformerNonces.fillQuoteTransformer, + data: encodeFillQuoteTransformerData({ + sellToken: intermediateToken, + buyToken, + side: FillQuoteTransformerSide.Sell, + fillAmount: MAX_UINT256, + maxOrderFillAmounts: [], + orders: [secondHopOrder], + signatures: [secondHopOrder.signature], + }), + }); + } else { + transforms.push({ + deploymentNonce: this.transformerNonces.fillQuoteTransformer, + data: encodeFillQuoteTransformerData({ + sellToken, + buyToken, + side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, + fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount, + maxOrderFillAmounts: [], + orders: quote.orders, + signatures: quote.orders.map(o => o.signature), + }), + }); + } if (isToETH) { // Create a WETH unwrapper if going to ETH. @@ -159,7 +186,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { transforms.push({ deploymentNonce: this.transformerNonces.payTakerTransformer, data: encodePayTakerTransformerData({ - tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS], + tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS].concat(quote.isTwoHop ? intermediateToken : []), amounts: [], }), }); @@ -201,15 +228,6 @@ function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { return quote.type === MarketOperation.Buy; } -function getTokenFromAssetData(assetData: string): string { - const data = assetDataUtils.decodeAssetDataOrThrow(assetData); - if (data.assetProxyId !== AssetProxyId.ERC20) { - throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`); - } - // tslint:disable-next-line:no-unnecessary-type-assertion - return (data as ERC20AssetData).tokenAddress; -} - /** * Find the nonce for a transformer given its deployer. * If `deployer` is the null address, zero will always be returned. diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 03c359fdd3..9baae9deab 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -2,7 +2,7 @@ import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/cont import { DevUtilsContract } from '@0x/contract-wrappers'; import { schemas } from '@0x/json-schemas'; import { assetDataUtils, SignedOrder } from '@0x/order-utils'; -import { APIOrder, MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook'; +import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook'; import { BigNumber, providerUtils } from '@0x/utils'; import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-types'; import * as _ from 'lodash'; @@ -165,6 +165,7 @@ export class SwapQuoter { samplerGasLimit, liquidityProviderRegistryAddress, rfqt, + tokenAdjacencyGraph, } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); const provider = providerUtils.standardizeOrThrow(supportedProvider); assert.isValidOrderbook('orderbook', orderbook); @@ -210,6 +211,7 @@ export class SwapQuoter { exchangeAddress: this._contractAddresses.exchange, }, liquidityProviderRegistryAddress, + tokenAdjacencyGraph, ); this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils); } @@ -422,7 +424,7 @@ export class SwapQuoter { const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); let [sellOrders, buyOrders] = options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native) - ? await Promise.resolve([[] as APIOrder[], [] as APIOrder[]]) + ? [[], []] : await Promise.all([ this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index bc7c175908..783e9e725a 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -2,7 +2,12 @@ import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-w import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types'; +import { + ERC20BridgeSource, + GetMarketOrdersOpts, + OptimizedMarketOrder, + TokenAdjacencyGraph, +} from './utils/market_operation_utils/types'; import { QuoteReport } from './utils/quote_report_generator'; import { LogFunction } from './utils/quote_requestor'; @@ -141,8 +146,6 @@ export interface ExchangeProxyContractOpts { affiliateFee: AffiliateFee; } -export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; - export interface GetExtensionContractTypeOpts { takerAddress?: string; ethAmount?: BigNumber; @@ -165,6 +168,7 @@ export interface SwapQuoteBase { worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown; quoteReport?: QuoteReport; + isTwoHop: boolean; } /** @@ -185,6 +189,8 @@ export interface MarketBuySwapQuote extends SwapQuoteBase { type: MarketOperation.Buy; } +export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; + /** * feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets. * takerAssetAmount: The amount of takerAsset swapped for desired makerAsset. @@ -205,9 +211,15 @@ export interface SwapQuoteInfo { /** * percentage breakdown of each liquidity source used in quote */ -export interface SwapQuoteOrdersBreakdown { - [source: string]: BigNumber; -} +export type SwapQuoteOrdersBreakdown = Partial< + { [key in Exclude]: BigNumber } & { + [ERC20BridgeSource.MultiHop]: { + proportion: BigNumber; + intermediateToken: string; + hops: ERC20BridgeSource[]; + }; + } +>; /** * nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity. @@ -272,6 +284,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { ethGasStationUrl?: string; rfqt?: SwapQuoterRfqtOpts; samplerOverrides?: SamplerOverrides; + tokenAdjacencyGraph?: TokenAdjacencyGraph; } /** diff --git a/packages/asset-swapper/src/utils/assert.ts b/packages/asset-swapper/src/utils/assert.ts index 377bdc6e88..04709c398c 100644 --- a/packages/asset-swapper/src/utils/assert.ts +++ b/packages/asset-swapper/src/utils/assert.ts @@ -19,12 +19,21 @@ export const assert = { sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData); sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData); sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema); - assert.isValidSwapQuoteOrders( - `${variableName}.orders`, - swapQuote.orders, - swapQuote.makerAssetData, - swapQuote.takerAssetData, - ); + if (swapQuote.isTwoHop) { + assert.isValidTwoHopSwapQuoteOrders( + `${variableName}.orders`, + swapQuote.orders, + swapQuote.makerAssetData, + swapQuote.takerAssetData, + ); + } else { + assert.isValidSwapQuoteOrders( + `${variableName}.orders`, + swapQuote.orders, + swapQuote.makerAssetData, + swapQuote.takerAssetData, + ); + } assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo); assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo); if (swapQuote.type === MarketOperation.Buy) { @@ -54,6 +63,28 @@ export const assert = { ); }); }, + isValidTwoHopSwapQuoteOrders( + variableName: string, + orders: SignedOrder[], + makerAssetData: string, + takerAssetData: string, + ): void { + assert.assert(orders.length === 2, `Expected ${variableName}.length to be 2 for a two-hop quote`); + assert.assert( + isAssetDataEquivalent(takerAssetData, orders[0].takerAssetData), + `Expected ${variableName}[0].takerAssetData to be ${takerAssetData} but found ${orders[0].takerAssetData}`, + ); + assert.assert( + isAssetDataEquivalent(makerAssetData, orders[1].makerAssetData), + `Expected ${variableName}[1].makerAssetData to be ${makerAssetData} but found ${orders[1].makerAssetData}`, + ); + assert.assert( + isAssetDataEquivalent(orders[0].makerAssetData, orders[1].takerAssetData), + `Expected ${variableName}[0].makerAssetData (${ + orders[0].makerAssetData + }) to equal ${variableName}[1].takerAssetData (${orders[1].takerAssetData})`, + ); + }, isValidOrdersForSwapQuoter(variableName: string, orders: T[]): void { _.every(orders, (order: T, index: number) => { assert.assert( diff --git a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts index a5e091c3a7..f4d5d99635 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts @@ -2,6 +2,10 @@ import { BigNumber } from '@0x/utils'; import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; import { Decimal } from 'decimal.js'; +import { ERC20BridgeSource } from './types'; + +// tslint:disable:boolean-naming + export interface BalancerPool { id: string; balanceIn: BigNumber; @@ -21,15 +25,15 @@ interface CacheValue { // tslint:disable:custom-no-magic-numbers const FIVE_SECONDS_MS = 5 * 1000; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 1000; -const MAX_POOLS_FETCHED = 2; +const MAX_POOLS_FETCHED = 3; const Decimal20 = Decimal.clone({ precision: 20 }); // tslint:enable:custom-no-magic-numbers export class BalancerPoolsCache { constructor( private readonly _cache: { [key: string]: CacheValue } = {}, - public cacheExpiryMs: number = FIVE_SECONDS_MS, private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED, ) {} @@ -42,10 +46,52 @@ export class BalancerPoolsCache { return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); } - protected async _getPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + public getCachedPoolAddressesForPair( + takerToken: string, + makerToken: string, + cacheExpiryMs?: number, + ): string[] | undefined { const key = JSON.stringify([takerToken, makerToken]); const value = this._cache[key]; - const minTimestamp = Date.now() - this.cacheExpiryMs; + if (cacheExpiryMs === undefined) { + return value === undefined ? [] : value.pools.map(pool => pool.id); + } + const minTimestamp = Date.now() - cacheExpiryMs; + if (value === undefined || value.timestamp < minTimestamp) { + return undefined; + } else { + return value.pools.map(pool => pool.id); + } + } + + public howToSampleBalancer( + takerToken: string, + makerToken: string, + excludedSources: ERC20BridgeSource[], + ): { onChain: boolean; offChain: boolean } { + // If Balancer is excluded as a source, do not sample. + if (excludedSources.includes(ERC20BridgeSource.Balancer)) { + return { onChain: false, offChain: false }; + } + const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); + // Sample Balancer on-chain (i.e. via the ERC20BridgeSampler contract) if: + // - Cached values are not stale + // - There is at least one Balancer pool for this pair + const onChain = cachedBalancerPools !== undefined && cachedBalancerPools.length > 0; + // Sample Balancer off-chain (i.e. via GraphQL query + `computeBalancerBuy/SellQuote`) + // if cached values are stale + const offChain = cachedBalancerPools === undefined; + return { onChain, offChain }; + } + + protected async _getPoolsForPairAsync( + takerToken: string, + makerToken: string, + cacheExpiryMs: number = FIVE_SECONDS_MS, + ): Promise { + const key = JSON.stringify([takerToken, makerToken]); + const value = this._cache[key]; + const minTimestamp = Date.now() - cacheExpiryMs; if (value === undefined || value.timestamp < minTimestamp) { const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); const timestamp = Date.now(); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/bancor_service.ts b/packages/asset-swapper/src/utils/market_operation_utils/bancor_service.ts index e4101850b1..95ddbb715b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/bancor_service.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/bancor_service.ts @@ -31,11 +31,7 @@ export class BancorService { return this._sdk; } - public async getQuoteAsync( - fromToken: string, - toToken: string, - amount: BigNumber = new BigNumber(1), - ): Promise> { + public async getQuoteAsync(fromToken: string, toToken: string, amount: BigNumber): Promise> { const sdk = await this.getSDKAsync(); const blockchain = sdk._core.blockchains[BlockchainType.Ethereum] as Ethereum; const sourceDecimals = await getDecimals(blockchain, fromToken); 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 c7f3e3186a..a8ca2c50a7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -138,6 +138,7 @@ export const ONE_ETHER = new BigNumber(1e18); export const NEGATIVE_INF = new BigNumber('-Infinity'); export const POSITIVE_INF = new BigNumber('Infinity'); export const ZERO_AMOUNT = new BigNumber(0); +export const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); export const ONE_HOUR_IN_SECONDS = 60 * 60; export const ONE_SECOND_MS = 1000; export const NULL_BYTES = '0x'; 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 e7fbf7bf6e..7ae13d60af 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -4,7 +4,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; -import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types'; +import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types'; // tslint:disable: prefer-for-of no-bitwise completed-docs @@ -155,6 +155,22 @@ function dexQuotesToPaths( return paths; } +export function getTwoHopAdjustedRate( + side: MarketOperation, + twoHopQuote: DexSample, + targetInput: BigNumber, + ethToOutputRate: BigNumber, + fees: FeeSchedule = {}, +): BigNumber { + const { output, input, fillData } = twoHopQuote; + if (input.isLessThan(targetInput) || output.isZero()) { + return ZERO_AMOUNT; + } + const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData)); + const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); + return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); +} + function sourceToFillFlags(source: ERC20BridgeSource): number { switch (source) { case ERC20BridgeSource.Uniswap: 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 01dee6ca1e..f8b8950c40 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -1,5 +1,4 @@ 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'; @@ -9,11 +8,20 @@ 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 { generateQuoteReport } from './../quote_report_generator'; +import { + BUY_SOURCES, + DEFAULT_GET_MARKET_ORDERS_OPTS, + FEE_QUOTE_SOURCES, + ONE_ETHER, + SELL_SOURCES, + ZERO_AMOUNT, +} from './constants'; import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; +import { getBestTwoHopQuote } from './multihop_utils'; import { createOrdersFromPath, + createOrdersFromTwoHopSample, createSignedOrdersFromRfqtIndicativeQuotes, createSignedOrdersWithFillableAmounts, getNativeOrderTokens, @@ -28,10 +36,13 @@ import { GetMarketOrdersOpts, MarketSideLiquidity, OptimizedMarketOrder, - OptimizedOrdersAndQuoteReport, + OptimizerResult, OrderDomain, + TokenAdjacencyGraph, } from './types'; +// tslint:disable:boolean-naming + /** * Returns a indicative quotes or an empty array if RFQT is not enabled or requested * @param makerAssetData the maker asset data @@ -70,6 +81,7 @@ export class MarketOperationUtils { private readonly contractAddresses: ContractAddresses, private readonly _orderDomain: OrderDomain, private readonly _liquidityProviderRegistry: string = NULL_ADDRESS, + private readonly _tokenAdjacencyGraph: TokenAdjacencyGraph = {}, ) { this._wethAddress = contractAddresses.etherToken.toLowerCase(); this._multiBridge = contractAddresses.multiBridge.toLowerCase(); @@ -94,54 +106,62 @@ export class MarketOperationUtils { const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase); + const { + onChain: sampleBalancerOnChain, + offChain: sampleBalancerOffChain, + } = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources); + // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. - DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), - // Get the custom liquidity provider from registry. - DexOrderSampler.ops.getLiquidityProviderFromRegistry( - this._liquidityProviderRegistry, - makerToken, - takerToken, - ), + this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), // Get ETH -> maker token price. - await DexOrderSampler.ops.getMedianSellRateAsync( + this._sampler.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _opts.excludedSources), makerToken, this._wethAddress, ONE_ETHER, this._wethAddress, - this._sampler.balancerPoolsCache, this._liquidityProviderRegistry, this._multiBridge, - this._sampler.bancorService, ), // Get ETH -> taker token price. - await DexOrderSampler.ops.getMedianSellRateAsync( + this._sampler.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _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( + this._sampler.getSellQuotes( difference( SELL_SOURCES.concat(this._optionalSources()), - _opts.excludedSources.concat(ERC20BridgeSource.Balancer), + _opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer), ), makerToken, takerToken, sampleAmounts, this._wethAddress, - this._sampler.balancerPoolsCache, this._liquidityProviderRegistry, this._multiBridge, - this._sampler.bancorService, ), + _opts.excludedSources.includes(ERC20BridgeSource.MultiHop) + ? DexOrderSampler.constant([]) + : this._sampler.getTwoHopSellQuotes( + difference( + SELL_SOURCES.concat(this._optionalSources()), + _opts.excludedSources.concat(ERC20BridgeSource.MultiBridge), + ), + makerToken, + takerToken, + takerAmount, + this._tokenAdjacencyGraph, + this._wethAddress, + this._liquidityProviderRegistry, + ), ); const rfqtPromise = getRfqtIndicativeQuotesAsync( @@ -152,45 +172,33 @@ export class MarketOperationUtils { _opts, ); - const balancerPromise = DexOrderSampler.ops - .getSellQuotesAsync( - difference([ERC20BridgeSource.Balancer], _opts.excludedSources), - makerToken, - takerToken, - sampleAmounts, - this._wethAddress, - this._sampler.balancerPoolsCache, - this._liquidityProviderRegistry, - this._multiBridge, - this._sampler.bancorService, - ) - .then(async r => this._sampler.executeAsync(r)); + const offChainBalancerPromise = sampleBalancerOffChain + ? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) + : Promise.resolve([]); + + const offChainBancorPromise = _opts.excludedSources.includes(ERC20BridgeSource.Bancor) + ? Promise.resolve([]) + : this._sampler.getBancorSellQuotesOffChainAsync(makerToken, takerToken, [takerAmount]); const [ - [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], + [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], rfqtIndicativeQuotes, - [balancerQuotes], - ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); + offChainBalancerQuotes, + offChainBancorQuotes, + ] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainBancorPromise]); - // 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), + dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, offChainBancorQuotes]), nativeOrders, orderFillableAmounts, ethToOutputRate: ethToMakerAssetRate, ethToInputRate: ethToTakerAssetRate, rfqtIndicativeQuotes, + twoHopQuotes, }; } @@ -213,69 +221,72 @@ export class MarketOperationUtils { const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); + const { + onChain: sampleBalancerOnChain, + offChain: sampleBalancerOffChain, + } = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources); + // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. - DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), - // Get the custom liquidity provider from registry. - DexOrderSampler.ops.getLiquidityProviderFromRegistry( - this._liquidityProviderRegistry, - makerToken, - takerToken, - ), - // Get ETH -> maker token price. - await DexOrderSampler.ops.getMedianSellRateAsync( + this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), + // Get ETH -> makerToken token price. + this._sampler.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _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( + this._sampler.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _opts.excludedSources), takerToken, this._wethAddress, ONE_ETHER, this._wethAddress, - this._sampler.balancerPoolsCache, this._liquidityProviderRegistry, this._multiBridge, - this._sampler.bancorService, ), // Get buy quotes for taker -> maker. - await DexOrderSampler.ops.getBuyQuotesAsync( + this._sampler.getBuyQuotes( difference( BUY_SOURCES.concat( this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [], ), - _opts.excludedSources.concat([ERC20BridgeSource.Balancer]), + _opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer), ), makerToken, takerToken, sampleAmounts, this._wethAddress, - this._sampler.balancerPoolsCache, this._liquidityProviderRegistry, - this._sampler.bancorService, ), + _opts.excludedSources.includes(ERC20BridgeSource.MultiHop) + ? DexOrderSampler.constant([]) + : this._sampler.getTwoHopBuyQuotes( + difference( + BUY_SOURCES.concat( + this._liquidityProviderRegistry !== NULL_ADDRESS + ? [ERC20BridgeSource.LiquidityProvider] + : [], + ), + _opts.excludedSources, + ), + makerToken, + takerToken, + makerAmount, + this._tokenAdjacencyGraph, + this._wethAddress, + this._liquidityProviderRegistry, + ), ); - const balancerPromise = this._sampler.executeAsync( - await DexOrderSampler.ops.getBuyQuotesAsync( - difference([ERC20BridgeSource.Balancer], _opts.excludedSources), - makerToken, - takerToken, - sampleAmounts, - this._wethAddress, - this._sampler.balancerPoolsCache, - this._liquidityProviderRegistry, - this._sampler.bancorService, - ), - ); + const offChainBalancerPromise = sampleBalancerOffChain + ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) + : Promise.resolve([]); const rfqtPromise = getRfqtIndicativeQuotesAsync( nativeOrders[0].makerAssetData, @@ -285,14 +296,10 @@ export class MarketOperationUtils { _opts, ); const [ - [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], + [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes], rfqtIndicativeQuotes, - [balancerQuotes], - ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); - // Attach the LiquidityProvider address to the sample fillData - (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach( - q => (q.fillData = { poolAddress: liquidityProviderAddress }), - ); + offChainBalancerQuotes, + ] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise]); // 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 }), @@ -302,12 +309,13 @@ export class MarketOperationUtils { inputAmount: makerAmount, inputToken: makerToken, outputToken: takerToken, - dexQuotes: dexQuotes.concat(balancerQuotes), + dexQuotes: dexQuotes.concat(offChainBalancerQuotes), nativeOrders, orderFillableAmounts, ethToOutputRate: ethToTakerAssetRate, ethToInputRate: ethToMakerAssetRate, rfqtIndicativeQuotes, + twoHopQuotes, }; } @@ -323,7 +331,7 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); return this._generateOptimizedOrdersAsync(marketSideLiquidity, { @@ -349,7 +357,7 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], makerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); return this._generateOptimizedOrdersAsync(marketSideLiquidity, { @@ -384,35 +392,29 @@ export class MarketOperationUtils { } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const sources = difference(BUY_SOURCES, _opts.excludedSources); + const sources = difference(BUY_SOURCES, _opts.excludedSources.concat(ERC20BridgeSource.Balancer)); const ops = [ ...batchNativeOrders.map(orders => - DexOrderSampler.ops.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange), + this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange), ), - ...(await Promise.all( - batchNativeOrders.map(async orders => - DexOrderSampler.ops.getMedianSellRateAsync( - difference(FEE_QUOTE_SOURCES, _opts.excludedSources), - getNativeOrderTokens(orders[0])[1], - this._wethAddress, - ONE_ETHER, - this._wethAddress, - this._sampler.balancerPoolsCache, - ), + ...batchNativeOrders.map(orders => + this._sampler.getMedianSellRate( + difference(FEE_QUOTE_SOURCES, _opts.excludedSources), + getNativeOrderTokens(orders[0])[1], + this._wethAddress, + ONE_ETHER, + this._wethAddress, ), - )), - ...(await Promise.all( - batchNativeOrders.map(async (orders, i) => - DexOrderSampler.ops.getBuyQuotesAsync( - sources, - getNativeOrderTokens(orders[0])[0], - getNativeOrderTokens(orders[0])[1], - [makerAmounts[i]], - this._wethAddress, - this._sampler.balancerPoolsCache, - ), + ), + ...batchNativeOrders.map((orders, i) => + this._sampler.getBuyQuotes( + sources, + getNativeOrderTokens(orders[0])[0], + getNativeOrderTokens(orders[0])[1], + [makerAmounts[i]], + this._wethAddress, ), - )), + ), ]; const executeResults = await this._sampler.executeBatchAsync(ops); @@ -444,6 +446,7 @@ export class MarketOperationUtils { rfqtIndicativeQuotes: [], inputToken: makerToken, outputToken: takerToken, + twoHopQuotes: [], }, { bridgeSlippage: _opts.bridgeSlippage, @@ -476,7 +479,7 @@ export class MarketOperationUtils { shouldBatchBridgeOrders?: boolean; quoteRequestor?: QuoteRequestor; }, - ): Promise { + ): Promise { const { inputToken, outputToken, @@ -488,8 +491,20 @@ export class MarketOperationUtils { dexQuotes, ethToOutputRate, ethToInputRate, + twoHopQuotes, } = marketSideLiquidity; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; + + const orderOpts = { + side, + inputToken, + outputToken, + orderDomain: this._orderDomain, + contractAddresses: this.contractAddresses, + bridgeSlippage: opts.bridgeSlippage || 0, + shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, + }; + // Convert native orders and dex quotes into fill paths. const paths = createFillPaths({ side, @@ -505,12 +520,33 @@ export class MarketOperationUtils { excludedSources: opts.excludedSources, feeSchedule: opts.feeSchedule, }); + // Find the optimal path. let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || []; if (optimalPath.length === 0) { throw new Error(AggregationError.NoOptimalPath); } - // Generate a fallback path if native orders are in the optimal paath. + const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount); + + const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( + marketSideLiquidity, + opts.feeSchedule, + ); + if (bestTwoHopRate.isGreaterThan(optimalPathRate)) { + const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote!, orderOpts); + const twoHopQuoteReport = generateQuoteReport( + side, + _.flatten(dexQuotes), + twoHopQuotes, + nativeOrders, + orderFillableAmounts, + bestTwoHopQuote!, + opts.quoteRequestor, + ); + return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true }; + } + + // Generate a fallback path if native orders are in the optimal path. const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native); if (opts.allowFallback && nativeSubPath.length !== 0) { // We create a fallback path that is exclusive of Native liquidity @@ -519,12 +555,7 @@ export class MarketOperationUtils { const nonNativeOptimalPath = (await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || []; // Calculate the slippage of on-chain sources compared to the most optimal path - const fallbackSlippage = getPathAdjustedSlippage( - side, - nonNativeOptimalPath, - inputAmount, - getPathAdjustedRate(side, optimalPath, inputAmount), - ); + const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate); if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) { // If the last fill is Native and penultimate is not, then the intention was to partial fill // In this case we drop it entirely as we can't handle a failure at the end and we don't @@ -542,24 +573,17 @@ export class MarketOperationUtils { optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; } } - const optimizedOrders = createOrdersFromPath(optimalPath, { - side, - inputToken, - outputToken, - orderDomain: this._orderDomain, - contractAddresses: this.contractAddresses, - bridgeSlippage: opts.bridgeSlippage || 0, - shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, - }); - const quoteReport = new QuoteReportGenerator( + const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts); + const quoteReport = generateQuoteReport( side, _.flatten(dexQuotes), + twoHopQuotes, nativeOrders, orderFillableAmounts, - _.flatten(optimizedOrders.map(o => o.fills)), + _.flatten(optimizedOrders.map(order => order.fills)), opts.quoteRequestor, - ).generateReport(); - return { optimizedOrders, quoteReport }; + ); + return { optimizedOrders, quoteReport, isTwoHop: false }; } private _optionalSources(): ERC20BridgeSource[] { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts new file mode 100644 index 0000000000..1f451c1b2e --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -0,0 +1,51 @@ +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ZERO_AMOUNT } from './constants'; +import { getTwoHopAdjustedRate } from './fills'; +import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types'; + +/** + * Given a token pair, returns the intermediate tokens to consider for two-hop routes. + */ +export function getIntermediateTokens( + makerToken: string, + takerToken: string, + tokenAdjacencyGraph: TokenAdjacencyGraph, + wethAddress: string, +): string[] { + let intermediateTokens = []; + if (makerToken === wethAddress) { + intermediateTokens = _.get(tokenAdjacencyGraph, takerToken, [] as string[]); + } else if (takerToken === wethAddress) { + intermediateTokens = _.get(tokenAdjacencyGraph, makerToken, [] as string[]); + } else { + intermediateTokens = _.union( + _.intersection(_.get(tokenAdjacencyGraph, takerToken, []), _.get(tokenAdjacencyGraph, makerToken, [])), + [wethAddress], + ); + } + return intermediateTokens.filter( + token => token.toLowerCase() !== makerToken.toLowerCase() && token.toLowerCase() !== takerToken.toLowerCase(), + ); +} + +/** + * Returns the best two-hop quote and the fee-adjusted rate of that quote. + */ +export function getBestTwoHopQuote( + marketSideLiquidity: MarketSideLiquidity, + feeSchedule?: FeeSchedule, +): { quote: DexSample | undefined; adjustedRate: BigNumber } { + const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity; + return twoHopQuotes + .map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule)) + .reduce( + (prev, curr, i) => + curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev, + { + adjustedRate: ZERO_AMOUNT, + quote: undefined as DexSample | undefined, + }, + ); +} 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 5d4b9ba5c4..62304e91f7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -8,6 +8,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { ERC20_PROXY_ID, + MAX_UINT256, NULL_ADDRESS, NULL_BYTES, ONE_HOUR_IN_SECONDS, @@ -23,10 +24,12 @@ import { BancorFillData, CollapsedFill, CurveFillData, + DexSample, ERC20BridgeSource, Fill, LiquidityProviderFillData, MultiBridgeFillData, + MultiHopFillData, NativeCollapsedFill, OptimizedMarketOrder, OrderDomain, @@ -150,6 +153,7 @@ export interface CreateOrderFromPathOpts { // Convert sell fills into orders. export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { + const [makerToken, takerToken] = getMakerTakerTokens(opts); const collapsedPath = collapsePath(path); const orders: OptimizedMarketOrder[] = []; for (let i = 0; i < collapsedPath.length; ) { @@ -168,7 +172,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts } // Always use DexForwarderBridge unless configured not to if (!opts.shouldBatchBridgeOrders) { - orders.push(createBridgeOrder(contiguousBridgeFills[0], opts)); + orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); i += 1; } else { orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); @@ -178,9 +182,36 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts return orders; } +export function createOrdersFromTwoHopSample( + sample: DexSample, + opts: CreateOrderFromPathOpts, +): OptimizedMarketOrder[] { + const [makerToken, takerToken] = getMakerTakerTokens(opts); + const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData!; + const firstHopFill: CollapsedFill = { + sourcePathId: '', + source: firstHopSource.source, + input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT, + output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output, + subFills: [], + fillData: firstHopSource.fillData, + }; + const secondHopFill: CollapsedFill = { + sourcePathId: '', + source: secondHopSource.source, + input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input, + output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256, + subFills: [], + fillData: secondHopSource.fillData, + }; + return [ + createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts), + createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts), + ]; +} + function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string { - const source = fill.source; - switch (source) { + switch (fill.source) { case ERC20BridgeSource.Eth2Dai: return opts.contractAddresses.eth2DaiBridge; case ERC20BridgeSource.Kyber: @@ -209,8 +240,12 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath throw new Error(AggregationError.NoBridgeForSource); } -function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { - const [makerToken, takerToken] = getMakerTakerTokens(opts); +function createBridgeOrder( + fill: CollapsedFill, + makerToken: string, + takerToken: string, + opts: CreateOrderFromPathOpts, +): OptimizedMarketOrder { const bridgeAddress = getBridgeAddressFromFill(fill, opts); let makerAssetData; @@ -277,7 +312,7 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): takerAssetAmount: slippedTakerAssetAmount, fillableMakerAssetAmount: slippedMakerAssetAmount, fillableTakerAssetAmount: slippedTakerAssetAmount, - ...createCommonBridgeOrderFields(opts), + ...createCommonBridgeOrderFields(opts.orderDomain), }; } @@ -290,7 +325,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP calls: [], }; for (const fill of fills) { - const bridgeOrder = createBridgeOrder(fill, opts); + const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts); totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( @@ -318,7 +353,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP takerAssetAmount: totalTakerAssetAmount, fillableMakerAssetAmount: totalMakerAssetAmount, fillableTakerAssetAmount: totalTakerAssetAmount, - ...createCommonBridgeOrderFields(opts), + ...createCommonBridgeOrderFields(opts.orderDomain), }; } @@ -395,7 +430,7 @@ function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFrom // Taker asset amount. opts.side === MarketOperation.Sell ? fill.input - : fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), + : BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256), ]; } @@ -414,7 +449,7 @@ type CommonBridgeOrderFields = Pick< > >; -function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { +function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields { return { takerAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS, @@ -428,7 +463,7 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri takerFee: ZERO_AMOUNT, fillableTakerFeeAmount: ZERO_AMOUNT, signature: WALLET_SIGNATURE, - ...opts.orderDomain, + ...orderDomain, }; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts index eb65fef5bf..4f2c9ae3d9 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts @@ -5,7 +5,7 @@ import { ERC20BridgeSamplerContract } from '../../wrappers'; import { BalancerPoolsCache } from './balancer_utils'; import { BancorService } from './bancor_service'; -import { samplerOperations } from './sampler_operations'; +import { SamplerOperations } from './sampler_operations'; import { BatchedOperation } from './types'; /** @@ -30,19 +30,15 @@ type BatchedOperationResult = T extends BatchedOperation ? TRe /** * Encapsulates interactions with the `ERC20BridgeSampler` contract. */ -export class DexOrderSampler { - /** - * Composable operations that can be batched in a single transaction, - * for use with `DexOrderSampler.executeAsync()`. - */ - public static ops = samplerOperations; - +export class DexOrderSampler extends SamplerOperations { constructor( - private readonly _samplerContract: ERC20BridgeSamplerContract, + _samplerContract: ERC20BridgeSamplerContract, private readonly _samplerOverrides?: SamplerOverrides, - public bancorService?: BancorService, - public balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), - ) {} + bancorService?: BancorService, + balancerPoolsCache?: BalancerPoolsCache, + ) { + super(_samplerContract, bancorService, balancerPoolsCache); + } /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ @@ -142,16 +138,14 @@ export class DexOrderSampler { * Takes an arbitrary length array, but is not typesafe. */ public async executeBatchAsync>>(ops: T): Promise { - const callDatas = ops.map(o => o.encodeCall(this._samplerContract)); + const callDatas = ops.map(o => o.encodeCall()); const { overrides, block } = this._samplerOverrides ? this._samplerOverrides : { overrides: undefined, block: undefined }; // All operations are NOOPs if (callDatas.every(cd => cd === NULL_BYTES)) { - return Promise.all( - callDatas.map(async (_callData, i) => ops[i].handleCallResultsAsync(this._samplerContract, NULL_BYTES)), - ); + return callDatas.map((_callData, i) => ops[i].handleCallResults(NULL_BYTES)); } // Execute all non-empty calldatas. const rawCallResults = await this._samplerContract @@ -159,11 +153,9 @@ export class DexOrderSampler { .callAsync({ overrides }, block); // Return the parsed results. let rawCallResultsIdx = 0; - return Promise.all( - callDatas.map(async (callData, i) => { - const result = callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : NULL_BYTES; - return ops[i].handleCallResultsAsync(this._samplerContract, result); - }), - ); + return callDatas.map((callData, i) => { + const result = callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : NULL_BYTES; + return ops[i].handleCallResults(result); + }); } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts new file mode 100644 index 0000000000..11bb096f01 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_contract_operation.ts @@ -0,0 +1,52 @@ +import { ContractFunctionObj } from '@0x/base-contract'; +import { BigNumber } from '@0x/utils'; + +import { ERC20BridgeSamplerContract } from '../../wrappers'; + +import { ERC20BridgeSource, FillData, SourceInfo, SourceQuoteOperation } from './types'; + +export type Parameters = T extends (...args: infer TArgs) => any ? TArgs : never; + +export interface SamplerContractCall< + TFunc extends (...args: any[]) => ContractFunctionObj, + TFillData extends FillData = FillData +> { + contract: ERC20BridgeSamplerContract; + function: TFunc; + params: Parameters; + callback?: (callResults: string, fillData: TFillData) => BigNumber[]; +} + +export class SamplerContractOperation< + TFunc extends (...args: any[]) => ContractFunctionObj, + TFillData extends FillData = FillData +> implements SourceQuoteOperation { + public readonly source: ERC20BridgeSource; + public fillData: TFillData; + private readonly _samplerContract: ERC20BridgeSamplerContract; + private readonly _samplerFunction: TFunc; + private readonly _params: Parameters; + private readonly _callback?: (callResults: string, fillData: TFillData) => BigNumber[]; + + constructor(opts: SourceInfo & SamplerContractCall) { + this.source = opts.source; + this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion + this._samplerContract = opts.contract; + this._samplerFunction = opts.function; + this._params = opts.params; + this._callback = opts.callback; + } + + public encodeCall(): string { + return this._samplerFunction + .bind(this._samplerContract)(...this._params) + .getABIEncodedTransactionData(); + } + public handleCallResults(callResults: string): BigNumber[] { + if (this._callback !== undefined) { + return this._callback(callResults, this.fillData); + } else { + return this._samplerContract.getABIDecodedReturnData(this._samplerFunction.name, callResults); + } + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index 74212dd254..e2311c8e11 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -1,12 +1,16 @@ +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; +import { ERC20BridgeSamplerContract } from '../../wrappers'; -import { BalancerPool, BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils'; +import { BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils'; import { BancorService } from './bancor_service'; -import { NULL_BYTES, ZERO_AMOUNT } from './constants'; +import { MAX_UINT256, NULL_BYTES, ZERO_AMOUNT } from './constants'; import { getCurveInfosForPair } from './curve_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils'; +import { getIntermediateTokens } from './multihop_utils'; +import { SamplerContractOperation } from './sampler_contract_operation'; import { BalancerFillData, BancorFillData, @@ -14,457 +18,609 @@ import { CurveFillData, CurveInfo, DexSample, - FillData, - Quote, + ERC20BridgeSource, + HopInfo, + LiquidityProviderFillData, + MultiBridgeFillData, + MultiHopFillData, SourceQuoteOperation, + TokenAdjacencyGraph, UniswapV2FillData, } from './types'; +// tslint:disable:no-inferred-empty-object-type no-unbound-method + /** * Composable operations that can be batched in a single transaction, * for use with `DexOrderSampler.executeAsync()`. */ -export const samplerOperations = { - getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { +export class SamplerOperations { + public static constant(result: T): BatchedOperation { return { - encodeCall: contract => { - return contract - .getOrderFillableTakerAssetAmounts(orders, orders.map(o => o.signature), exchangeAddress) - .getABIEncodedTransactionData(); + encodeCall: () => { + return '0x'; }, - handleCallResultsAsync: async (contract, callResults) => { - return contract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', callResults); + handleCallResults: _callResults => { + return result; }, }; - }, - getOrderFillableMakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { - return { - encodeCall: contract => { - return contract - .getOrderFillableMakerAssetAmounts(orders, orders.map(o => o.signature), exchangeAddress) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract.getABIDecodedReturnData('getOrderFillableMakerAssetAmounts', callResults); - }, - }; - }, - getKyberSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.Kyber, - encodeCall: contract => { - return contract - .sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromKyberNetwork', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getKyberBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.Kyber, - encodeCall: contract => { - return contract - .sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromKyberNetwork', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getUniswapSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.Uniswap, - encodeCall: contract => { - return contract - .sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromUniswap', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getUniswapBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.Uniswap, - encodeCall: contract => { - return contract - .sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromUniswap', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getUniswapV2SellQuotes( - tokenAddressPath: string[], - takerFillAmounts: BigNumber[], - ): SourceQuoteOperation { - return { - source: ERC20BridgeSource.UniswapV2, - encodeCall: contract => { - return contract - .sampleSellsFromUniswapV2(tokenAddressPath, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromUniswapV2', callResults) - .map(amount => ({ - amount, - fillData: { tokenAddressPath }, - })); - }, - }; - }, - getUniswapV2BuyQuotes( - tokenAddressPath: string[], - makerFillAmounts: BigNumber[], - ): SourceQuoteOperation { - return { - source: ERC20BridgeSource.UniswapV2, - encodeCall: contract => { - return contract - .sampleBuysFromUniswapV2(tokenAddressPath, makerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromUniswapV2', callResults) - .map(amount => ({ - amount, - fillData: { tokenAddressPath }, - })); - }, - }; - }, - getLiquidityProviderSellQuotes( - registryAddress: string, + } + + constructor( + protected readonly _samplerContract: ERC20BridgeSamplerContract, + public readonly bancorService?: BancorService, + public readonly balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), + ) {} + + public getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Native, + contract: this._samplerContract, + function: this._samplerContract.getOrderFillableTakerAssetAmounts, + params: [orders, orders.map(o => o.signature), exchangeAddress], + }); + } + + public getOrderFillableMakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Native, + contract: this._samplerContract, + function: this._samplerContract.getOrderFillableMakerAssetAmounts, + params: [orders, orders.map(o => o.signature), exchangeAddress], + }); + } + + public getKyberSellQuotes( makerToken: string, takerToken: string, takerFillAmounts: BigNumber[], ): SourceQuoteOperation { - return { - source: ERC20BridgeSource.LiquidityProvider, - encodeCall: contract => { - return contract - .sampleSellsFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromLiquidityProviderRegistry', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getLiquidityProviderBuyQuotes( - registryAddress: string, + return new SamplerContractOperation({ + source: ERC20BridgeSource.Kyber, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromKyberNetwork, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getKyberBuyQuotes( makerToken: string, takerToken: string, makerFillAmounts: BigNumber[], ): SourceQuoteOperation { - return { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Kyber, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromKyberNetwork, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + + public getUniswapSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Uniswap, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromUniswap, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getUniswapBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Uniswap, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromUniswap, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + + public getUniswapV2SellQuotes( + tokenAddressPath: string[], + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.UniswapV2, + fillData: { tokenAddressPath }, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromUniswapV2, + params: [tokenAddressPath, takerFillAmounts], + }); + } + + public getUniswapV2BuyQuotes( + tokenAddressPath: string[], + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.UniswapV2, + fillData: { tokenAddressPath }, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromUniswapV2, + params: [tokenAddressPath, makerFillAmounts], + }); + } + + public getLiquidityProviderSellQuotes( + registryAddress: string, + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.LiquidityProvider, - encodeCall: contract => { - return contract - .sampleBuysFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); + fillData: {} as LiquidityProviderFillData, // tslint:disable-line:no-object-literal-type-assertion + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromLiquidityProviderRegistry, + params: [registryAddress, takerToken, makerToken, takerFillAmounts], + callback: (callResults: string, fillData: LiquidityProviderFillData): BigNumber[] => { + const [samples, poolAddress] = this._samplerContract.getABIDecodedReturnData<[BigNumber[], string]>( + 'sampleSellsFromLiquidityProviderRegistry', + callResults, + ); + fillData.poolAddress = poolAddress; + return samples; }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromLiquidityProviderRegistry', callResults) - .map(amount => ({ amount })); + }); + } + + public getLiquidityProviderBuyQuotes( + registryAddress: string, + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.LiquidityProvider, + fillData: {} as LiquidityProviderFillData, // tslint:disable-line:no-object-literal-type-assertion + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromLiquidityProviderRegistry, + params: [registryAddress, takerToken, makerToken, makerFillAmounts], + callback: (callResults: string, fillData: LiquidityProviderFillData): BigNumber[] => { + const [samples, poolAddress] = this._samplerContract.getABIDecodedReturnData<[BigNumber[], string]>( + 'sampleBuysFromLiquidityProviderRegistry', + callResults, + ); + fillData.poolAddress = poolAddress; + return samples; }, - }; - }, - getMultiBridgeSellQuotes( + }); + } + + public getMultiBridgeSellQuotes( multiBridgeAddress: string, makerToken: string, intermediateToken: string, takerToken: string, takerFillAmounts: BigNumber[], - ): SourceQuoteOperation { - return { + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.MultiBridge, - encodeCall: contract => { - return contract - .sampleSellsFromMultiBridge( - multiBridgeAddress, - takerToken, - intermediateToken, - makerToken, - takerFillAmounts, - ) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromMultiBridge', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getEth2DaiSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { + fillData: { poolAddress: multiBridgeAddress }, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromMultiBridge, + params: [multiBridgeAddress, takerToken, intermediateToken, makerToken, takerFillAmounts], + }); + } + + public getEth2DaiSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.Eth2Dai, - encodeCall: contract => { - return contract - .sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromEth2Dai', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getEth2DaiBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromEth2Dai, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getEth2DaiBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.Eth2Dai, - encodeCall: contract => { - return contract - .sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromEth2Dai', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getCurveSellQuotes( + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromEth2Dai, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + + public getCurveSellQuotes( curve: CurveInfo, fromTokenIdx: number, toTokenIdx: number, takerFillAmounts: BigNumber[], ): SourceQuoteOperation { - return { + return new SamplerContractOperation({ source: ERC20BridgeSource.Curve, - encodeCall: contract => { - return contract - .sampleSellsFromCurve( - { - poolAddress: curve.poolAddress, - sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector, - buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector, - }, - new BigNumber(fromTokenIdx), - new BigNumber(toTokenIdx), - takerFillAmounts, - ) - .getABIEncodedTransactionData(); + fillData: { + curve, + fromTokenIdx, + toTokenIdx, }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromCurve', callResults) - .map(amount => ({ - amount, - fillData: { - curve, - fromTokenIdx, - toTokenIdx, - }, - })); - }, - }; - }, - getCurveBuyQuotes( + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromCurve, + params: [ + { + poolAddress: curve.poolAddress, + sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector, + buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector, + }, + new BigNumber(fromTokenIdx), + new BigNumber(toTokenIdx), + takerFillAmounts, + ], + }); + } + + public getCurveBuyQuotes( curve: CurveInfo, fromTokenIdx: number, toTokenIdx: number, makerFillAmounts: BigNumber[], ): SourceQuoteOperation { - return { + return new SamplerContractOperation({ source: ERC20BridgeSource.Curve, - encodeCall: contract => { - return contract - .sampleBuysFromCurve( - { - poolAddress: curve.poolAddress, - sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector, - buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector, - }, - new BigNumber(fromTokenIdx), - new BigNumber(toTokenIdx), - makerFillAmounts, - ) - .getABIEncodedTransactionData(); + fillData: { + curve, + fromTokenIdx, + toTokenIdx, }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromCurve', callResults) - .map(amount => ({ - amount, - fillData: { - curve, - fromTokenIdx, - toTokenIdx, - }, - })); - }, - }; - }, - getBancorSellQuotes( + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromCurve, + params: [ + { + poolAddress: curve.poolAddress, + sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector, + buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector, + }, + new BigNumber(fromTokenIdx), + new BigNumber(toTokenIdx), + makerFillAmounts, + ], + }); + } + + public getBalancerSellQuotes( + poolAddress: string, makerToken: string, takerToken: string, takerFillAmounts: BigNumber[], - bancorService: BancorService, - ): SourceQuoteOperation { - return { - source: ERC20BridgeSource.Bancor, - encodeCall: _contract => { - return '0x'; - }, - handleCallResultsAsync: async (_contract, _callResults) => { - return Promise.all( - takerFillAmounts.map(async amt => bancorService.getQuoteAsync(takerToken, makerToken, amt)), - ); - }, - }; - }, - getBalancerSellQuotes(pool: BalancerPool, takerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.Balancer, - ...samplerOperations.constant( - takerFillAmounts.map(amount => ({ - amount: computeBalancerSellQuote(pool, amount), - fillData: { poolAddress: pool.id }, - })), - ), - }; - }, - getBalancerBuyQuotes(pool: BalancerPool, makerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { + fillData: { poolAddress }, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromBalancer, + params: [poolAddress, takerToken, makerToken, takerFillAmounts], + }); + } + + public getBalancerBuyQuotes( + poolAddress: string, + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.Balancer, - ...samplerOperations.constant( - makerFillAmounts.map(amount => ({ - amount: computeBalancerBuyQuote(pool, amount), - fillData: { poolAddress: pool.id }, - })), - ), - }; - }, - getMStableSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.MStable, - encodeCall: contract => { - return contract - .sampleSellsFromMStable(takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromMStable', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getMStableBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { - return { - source: ERC20BridgeSource.MStable, - encodeCall: contract => { - return contract - .sampleBuysFromMStable(takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromMStable', callResults) - .map(amount => ({ amount })); - }, - }; - }, - getMooniswapSellQuotes( + fillData: { poolAddress }, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromBalancer, + params: [poolAddress, takerToken, makerToken, makerFillAmounts], + }); + } + + public async getBalancerSellQuotesOffChainAsync( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): Promise>>> { + const pools = await this.balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); + return pools.map(pool => + takerFillAmounts.map(amount => ({ + source: ERC20BridgeSource.Balancer, + output: computeBalancerSellQuote(pool, amount), + input: amount, + fillData: { poolAddress: pool.id }, + })), + ); + } + + public async getBalancerBuyQuotesOffChainAsync( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): Promise>>> { + const pools = await this.balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); + return pools.map(pool => + makerFillAmounts.map(amount => ({ + source: ERC20BridgeSource.Balancer, + output: computeBalancerBuyQuote(pool, amount), + input: amount, + fillData: { poolAddress: pool.id }, + })), + ); + } + + public getMStableSellQuotes( makerToken: string, takerToken: string, takerFillAmounts: BigNumber[], ): SourceQuoteOperation { - return { + return new SamplerContractOperation({ + source: ERC20BridgeSource.MStable, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromMStable, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getMStableBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.MStable, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromMStable, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + + public async getBancorSellQuotesOffChainAsync( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): Promise>> { + if (this.bancorService === undefined) { + throw new Error('Cannot sample liquidity from Bancor; no Bancor service instantiated.'); + } + return Promise.all( + takerFillAmounts.map(async amount => { + try { + const { amount: output, fillData } = await this.bancorService!.getQuoteAsync( + takerToken, + makerToken, + amount, + ); + return { + source: ERC20BridgeSource.Bancor, + output, + input: amount, + fillData, + }; + } catch (e) { + return { + source: ERC20BridgeSource.Bancor, + output: ZERO_AMOUNT, + input: amount, + fillData: { path: [], networkAddress: '' }, + }; + } + }), + ); + } + + public getMooniswapSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ source: ERC20BridgeSource.Mooniswap, - encodeCall: contract => { - return contract - .sampleSellsFromMooniswap(takerToken, makerToken, takerFillAmounts) - .getABIEncodedTransactionData(); + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromMooniswap, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getMooniswapBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Mooniswap, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromMooniswap, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + + public getTwoHopSellQuotes( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + sellAmount: BigNumber, + tokenAdjacencyGraph: TokenAdjacencyGraph, + wethAddress: string, + liquidityProviderRegistryAddress?: string, + ): BatchedOperation>> { + const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress); + const subOps = intermediateTokens.map(intermediateToken => { + const firstHopOps = this._getSellQuoteOperations( + sources, + intermediateToken, + takerToken, + [ZERO_AMOUNT], + wethAddress, + liquidityProviderRegistryAddress, + ); + const secondHopOps = this._getSellQuoteOperations( + sources, + makerToken, + intermediateToken, + [ZERO_AMOUNT], + wethAddress, + liquidityProviderRegistryAddress, + ); + return new SamplerContractOperation({ + contract: this._samplerContract, + source: ERC20BridgeSource.MultiHop, + function: this._samplerContract.sampleTwoHopSell, + params: [firstHopOps.map(op => op.encodeCall()), secondHopOps.map(op => op.encodeCall()), sellAmount], + fillData: { intermediateToken } as MultiHopFillData, // tslint:disable-line:no-object-literal-type-assertion + callback: (callResults: string, fillData: MultiHopFillData): BigNumber[] => { + const [firstHop, secondHop, buyAmount] = this._samplerContract.getABIDecodedReturnData< + [HopInfo, HopInfo, BigNumber] + >('sampleTwoHopSell', callResults); + if (buyAmount.isZero()) { + return [ZERO_AMOUNT]; + } + fillData.firstHopSource = firstHopOps[firstHop.sourceIndex.toNumber()]; + fillData.secondHopSource = secondHopOps[secondHop.sourceIndex.toNumber()]; + fillData.firstHopSource.handleCallResults(firstHop.returnData); + fillData.secondHopSource.handleCallResults(secondHop.returnData); + return [buyAmount]; + }, + }); + }); + return { + encodeCall: () => { + const subCalls = subOps.map(op => op.encodeCall()); + return this._samplerContract.batchCall(subCalls).getABIEncodedTransactionData(); }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleSellsFromMooniswap', callResults) - .map(amount => ({ amount })); + handleCallResults: callResults => { + const rawSubCallResults = this._samplerContract.getABIDecodedReturnData( + 'batchCall', + callResults, + ); + return subOps.map((op, i) => { + const [output] = op.handleCallResults(rawSubCallResults[i]); + return { + source: op.source, + output, + input: sellAmount, + fillData: op.fillData, + }; + }); }, }; - }, - getMooniswapBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { + } + + public getTwoHopBuyQuotes( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + buyAmount: BigNumber, + tokenAdjacencyGraph: TokenAdjacencyGraph, + wethAddress: string, + liquidityProviderRegistryAddress?: string, + ): BatchedOperation>> { + const intermediateTokens = getIntermediateTokens(makerToken, takerToken, tokenAdjacencyGraph, wethAddress); + const subOps = intermediateTokens.map(intermediateToken => { + const firstHopOps = this._getBuyQuoteOperations( + sources, + intermediateToken, + takerToken, + [new BigNumber(0)], + wethAddress, + liquidityProviderRegistryAddress, + ); + const secondHopOps = this._getBuyQuoteOperations( + sources, + makerToken, + intermediateToken, + [new BigNumber(0)], + wethAddress, + liquidityProviderRegistryAddress, + ); + return new SamplerContractOperation({ + contract: this._samplerContract, + source: ERC20BridgeSource.MultiHop, + function: this._samplerContract.sampleTwoHopBuy, + params: [firstHopOps.map(op => op.encodeCall()), secondHopOps.map(op => op.encodeCall()), buyAmount], + fillData: { intermediateToken } as MultiHopFillData, // tslint:disable-line:no-object-literal-type-assertion + callback: (callResults: string, fillData: MultiHopFillData): BigNumber[] => { + const [firstHop, secondHop, sellAmount] = this._samplerContract.getABIDecodedReturnData< + [HopInfo, HopInfo, BigNumber] + >('sampleTwoHopBuy', callResults); + if (sellAmount.isEqualTo(MAX_UINT256)) { + return [sellAmount]; + } + fillData.firstHopSource = firstHopOps[firstHop.sourceIndex.toNumber()]; + fillData.secondHopSource = secondHopOps[secondHop.sourceIndex.toNumber()]; + fillData.firstHopSource.handleCallResults(firstHop.returnData); + fillData.secondHopSource.handleCallResults(secondHop.returnData); + return [sellAmount]; + }, + }); + }); return { - source: ERC20BridgeSource.Mooniswap, - encodeCall: contract => { - return contract - .sampleBuysFromMooniswap(takerToken, makerToken, makerFillAmounts) - .getABIEncodedTransactionData(); + encodeCall: () => { + const subCalls = subOps.map(op => op.encodeCall()); + return this._samplerContract.batchCall(subCalls).getABIEncodedTransactionData(); }, - handleCallResultsAsync: async (contract, callResults) => { - return contract - .getABIDecodedReturnData('sampleBuysFromMooniswap', callResults) - .map(amount => ({ amount })); + handleCallResults: callResults => { + const rawSubCallResults = this._samplerContract.getABIDecodedReturnData( + 'batchCall', + callResults, + ); + return subOps.map((op, i) => { + const [output] = op.handleCallResults(rawSubCallResults[i]); + return { + source: op.source, + output, + input: buyAmount, + fillData: op.fillData, + }; + }); }, }; - }, - getMedianSellRateAsync: async ( + } + + public getMedianSellRate( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, takerFillAmount: BigNumber, wethAddress: string, - balancerPoolsCache?: BalancerPoolsCache, liquidityProviderRegistryAddress?: string, multiBridgeAddress?: string, - bancorService?: BancorService, - ): Promise> => { + ): BatchedOperation { if (makerToken.toLowerCase() === takerToken.toLowerCase()) { - return samplerOperations.constant(new BigNumber(1)); + return SamplerOperations.constant(new BigNumber(1)); } - const getSellQuotes = await samplerOperations.getSellQuotesAsync( + const getSellQuotes = this.getSellQuotes( sources, makerToken, takerToken, [takerFillAmount], wethAddress, - balancerPoolsCache, liquidityProviderRegistryAddress, multiBridgeAddress, - bancorService, ); return { - encodeCall: contract => { - const encodedCall = getSellQuotes.encodeCall(contract); + encodeCall: () => { + const encodedCall = getSellQuotes.encodeCall(); // All soures were excluded if (encodedCall === NULL_BYTES) { return NULL_BYTES; } - const subCalls = [getSellQuotes.encodeCall(contract)]; - return contract.batchCall(subCalls).getABIEncodedTransactionData(); + return this._samplerContract.batchCall([encodedCall]).getABIEncodedTransactionData(); }, - handleCallResultsAsync: async (contract, callResults) => { + handleCallResults: callResults => { if (callResults === NULL_BYTES) { return ZERO_AMOUNT; } - const rawSubCallResults = contract.getABIDecodedReturnData('batchCall', callResults); - const samples = await getSellQuotes.handleCallResultsAsync(contract, rawSubCallResults[0]); + const rawSubCallResults = this._samplerContract.getABIDecodedReturnData( + 'batchCall', + callResults, + ); + const samples = getSellQuotes.handleCallResults(rawSubCallResults[0]); if (samples.length === 0) { return ZERO_AMOUNT; } @@ -479,312 +635,234 @@ export const samplerOperations = { return medianSample.output.div(medianSample.input); }, }; - }, - constant(result: T): BatchedOperation { - return { - encodeCall: _contract => { - return NULL_BYTES; - }, - handleCallResultsAsync: async (_contract, _callResults) => { - return result; - }, - }; - }, - getLiquidityProviderFromRegistry( - registryAddress: string, - makerToken: string, - takerToken: string, - ): BatchedOperation { - return { - encodeCall: contract => { - return contract - .getLiquidityProviderFromRegistry(registryAddress, takerToken, makerToken) - .getABIEncodedTransactionData(); - }, - handleCallResultsAsync: async (contract, callResults) => { - return contract.getABIDecodedReturnData('getLiquidityProviderFromRegistry', callResults); - }, - }; - }, - getSellQuotesAsync: async ( + } + + public getSellQuotes( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, takerFillAmounts: BigNumber[], wethAddress: string, - balancerPoolsCache?: BalancerPoolsCache, liquidityProviderRegistryAddress?: string, multiBridgeAddress?: string, - bancorService?: BancorService, - ): Promise> => { - const subOps = _.flatten( - await Promise.all( - sources.map( - async (source): Promise | Array>> => { - switch (source) { - case ERC20BridgeSource.Eth2Dai: - return samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts); - case ERC20BridgeSource.Uniswap: - return samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); - case ERC20BridgeSource.UniswapV2: - const ops = [ - samplerOperations.getUniswapV2SellQuotes( - [takerToken, makerToken], - takerFillAmounts, - ), - ]; - if (takerToken !== wethAddress && makerToken !== wethAddress) { - ops.push( - samplerOperations.getUniswapV2SellQuotes( - [takerToken, wethAddress, makerToken], - takerFillAmounts, - ), - ); - } - return ops; - case ERC20BridgeSource.Kyber: - return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); - case ERC20BridgeSource.Curve: - return getCurveInfosForPair(takerToken, makerToken).map(curve => - samplerOperations.getCurveSellQuotes( - curve, - curve.tokens.indexOf(takerToken), - curve.tokens.indexOf(makerToken), - takerFillAmounts, - ), - ); - case ERC20BridgeSource.LiquidityProvider: - if (liquidityProviderRegistryAddress === undefined) { - throw new Error( - 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', - ); - } - return samplerOperations.getLiquidityProviderSellQuotes( - liquidityProviderRegistryAddress, - makerToken, - takerToken, - takerFillAmounts, - ); - case ERC20BridgeSource.MultiBridge: - if (multiBridgeAddress === undefined) { - throw new Error( - 'Cannot sample liquidity from MultiBridge if an address is not provided.', - ); - } - const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); - return samplerOperations.getMultiBridgeSellQuotes( - multiBridgeAddress, - makerToken, - intermediateToken, - takerToken, - takerFillAmounts, - ); - // todo: refactor sampler ops to share state with DexOrderSampler so cache doesn't have to be passed as a param - case ERC20BridgeSource.Balancer: - if (balancerPoolsCache === undefined) { - throw new Error( - 'Cannot sample liquidity from Balancer if a cache is not provided.', - ); - } - const pools = await balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return pools.map(pool => - samplerOperations.getBalancerSellQuotes(pool, takerFillAmounts), - ); - case ERC20BridgeSource.Bancor: - if (bancorService === undefined) { - throw new Error( - 'Cannot sample liquidity from Bancor; no Bancor service instantiated.', - ); - } - return samplerOperations.getBancorSellQuotes( - makerToken, - takerToken, - takerFillAmounts, - bancorService, - ); - case ERC20BridgeSource.MStable: - return samplerOperations.getMStableSellQuotes(makerToken, takerToken, takerFillAmounts); - case ERC20BridgeSource.Mooniswap: - return samplerOperations.getMooniswapSellQuotes( - makerToken, - takerToken, - takerFillAmounts, - ); - default: - throw new Error(`Unsupported sell sample source: ${source}`); - } - }, - ), - ), + ): BatchedOperation { + const subOps = this._getSellQuoteOperations( + sources, + makerToken, + takerToken, + takerFillAmounts, + wethAddress, + liquidityProviderRegistryAddress, + multiBridgeAddress, ); - const nonSamplerSources = [ERC20BridgeSource.Balancer, ERC20BridgeSource.Bancor]; - const samplerOps: Array> = []; - const nonSamplerOps: Array> = []; - subOps.forEach(op => { - if (nonSamplerSources.includes(op.source)) { - nonSamplerOps.push(op); - } else { - samplerOps.push(op); - } - }); return { - encodeCall: contract => { - // All operations are NOOPs - if (samplerOps.length === 0) { - return NULL_BYTES; - } - const subCalls = samplerOps.map(op => op.encodeCall(contract)); - return contract.batchCall(subCalls).getABIEncodedTransactionData(); + encodeCall: () => { + const subCalls = subOps.map(op => op.encodeCall()); + return this._samplerContract.batchCall(subCalls).getABIEncodedTransactionData(); }, - handleCallResultsAsync: async (contract, callResults) => { - let samples: Array>>; - // If all operations were NOOPs then just call the handle result callback - if (callResults === NULL_BYTES && samplerOps.length === 0) { - samples = await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))); - } else { - const rawSubCallResults = contract.getABIDecodedReturnData('batchCall', callResults); - samples = await Promise.all( - samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), - ); - samples = samples.concat( - await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))), - ); - } - return [...samplerOps, ...nonSamplerOps].map((op, i) => { + handleCallResults: callResults => { + const rawSubCallResults = this._samplerContract.getABIDecodedReturnData( + 'batchCall', + callResults, + ); + const samples = subOps.map((op, i) => op.handleCallResults(rawSubCallResults[i])); + return subOps.map((op, i) => { return samples[i].map((output, j) => ({ source: op.source, - output: output.amount, + output, input: takerFillAmounts[j], - fillData: output.fillData, + fillData: op.fillData, })); }); }, }; - }, - getBuyQuotesAsync: async ( + } + + public getBuyQuotes( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, makerFillAmounts: BigNumber[], wethAddress: string, - balancerPoolsCache?: BalancerPoolsCache, liquidityProviderRegistryAddress?: string, - bancorService?: BancorService, - ): Promise> => { - const subOps = _.flatten( - await Promise.all( - sources.map( - async (source): Promise | Array>> => { - switch (source) { - case ERC20BridgeSource.Eth2Dai: - return samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts); - case ERC20BridgeSource.Uniswap: - return samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); - case ERC20BridgeSource.UniswapV2: - const ops = [ - samplerOperations.getUniswapV2BuyQuotes([takerToken, makerToken], makerFillAmounts), - ]; - if (takerToken !== wethAddress && makerToken !== wethAddress) { - ops.push( - samplerOperations.getUniswapV2BuyQuotes( - [takerToken, wethAddress, makerToken], - makerFillAmounts, - ), - ); - } - return ops; - case ERC20BridgeSource.Kyber: - return samplerOperations.getKyberBuyQuotes(makerToken, takerToken, makerFillAmounts); - case ERC20BridgeSource.Curve: - return getCurveInfosForPair(takerToken, makerToken).map(curve => - samplerOperations.getCurveBuyQuotes( - curve, - curve.tokens.indexOf(takerToken), - curve.tokens.indexOf(makerToken), - makerFillAmounts, - ), - ); - case ERC20BridgeSource.LiquidityProvider: - if (liquidityProviderRegistryAddress === undefined) { - throw new Error( - 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', - ); - } - return samplerOperations.getLiquidityProviderBuyQuotes( - liquidityProviderRegistryAddress, - makerToken, - takerToken, - makerFillAmounts, - ); - case ERC20BridgeSource.Balancer: - if (balancerPoolsCache === undefined) { - throw new Error( - 'Cannot sample liquidity from Balancer if a cache is not provided.', - ); - } - const pools = await balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return pools.map(pool => - samplerOperations.getBalancerBuyQuotes(pool, makerFillAmounts), - ); - case ERC20BridgeSource.Bancor: - return []; // FIXME: Waiting for Bancor SDK to support buy quotes, but don't throw an error here - case ERC20BridgeSource.MStable: - return samplerOperations.getMStableBuyQuotes(makerToken, takerToken, makerFillAmounts); - case ERC20BridgeSource.Mooniswap: - return samplerOperations.getMooniswapBuyQuotes( - makerToken, - takerToken, - makerFillAmounts, - ); - default: - throw new Error(`Unsupported buy sample source: ${source}`); - } - }, - ), - ), + ): BatchedOperation { + const subOps = this._getBuyQuoteOperations( + sources, + makerToken, + takerToken, + makerFillAmounts, + wethAddress, + liquidityProviderRegistryAddress, ); - const nonSamplerSources = [ERC20BridgeSource.Balancer, ERC20BridgeSource.Bancor]; - const samplerOps: Array> = []; - const nonSamplerOps: Array> = []; - subOps.forEach(op => { - if (nonSamplerSources.find(s => s === op.source) !== undefined) { - nonSamplerOps.push(op); - } else { - samplerOps.push(op); - } - }); return { - encodeCall: contract => { - // All operations are NOOPs - if (samplerOps.length === 0) { - return NULL_BYTES; - } - const subCalls = samplerOps.map(op => op.encodeCall(contract)); - return contract.batchCall(subCalls).getABIEncodedTransactionData(); + encodeCall: () => { + const subCalls = subOps.map(op => op.encodeCall()); + return this._samplerContract.batchCall(subCalls).getABIEncodedTransactionData(); }, - handleCallResultsAsync: async (contract, callResults) => { - let samples: Array>>; - if (callResults === NULL_BYTES && samplerOps.length === 0) { - samples = await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))); - } else { - const rawSubCallResults = contract.getABIDecodedReturnData('batchCall', callResults); - samples = await Promise.all( - samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), - ); - samples = samples.concat( - await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))), - ); - } - return [...samplerOps, ...nonSamplerOps].map((op, i) => { + handleCallResults: callResults => { + const rawSubCallResults = this._samplerContract.getABIDecodedReturnData( + 'batchCall', + callResults, + ); + const samples = subOps.map((op, i) => op.handleCallResults(rawSubCallResults[i])); + return subOps.map((op, i) => { return samples[i].map((output, j) => ({ source: op.source, - output: output.amount, + output, input: makerFillAmounts[j], - fillData: output.fillData, + fillData: op.fillData, })); }); }, }; - }, -}; + } + + private _getSellQuoteOperations( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + wethAddress: string, + liquidityProviderRegistryAddress?: string, + multiBridgeAddress?: string, + ): SourceQuoteOperation[] { + return _.flatten( + sources.map( + (source): SourceQuoteOperation | SourceQuoteOperation[] => { + switch (source) { + case ERC20BridgeSource.Eth2Dai: + return this.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts); + case ERC20BridgeSource.Uniswap: + return this.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); + case ERC20BridgeSource.UniswapV2: + const ops = [this.getUniswapV2SellQuotes([takerToken, makerToken], takerFillAmounts)]; + if (takerToken !== wethAddress && makerToken !== wethAddress) { + ops.push( + this.getUniswapV2SellQuotes( + [takerToken, wethAddress, makerToken], + takerFillAmounts, + ), + ); + } + return ops; + case ERC20BridgeSource.Kyber: + return this.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); + case ERC20BridgeSource.Curve: + return getCurveInfosForPair(takerToken, makerToken).map(curve => + this.getCurveSellQuotes( + curve, + curve.tokens.indexOf(takerToken), + curve.tokens.indexOf(makerToken), + takerFillAmounts, + ), + ); + case ERC20BridgeSource.LiquidityProvider: + if (liquidityProviderRegistryAddress === undefined) { + throw new Error( + 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', + ); + } + return this.getLiquidityProviderSellQuotes( + liquidityProviderRegistryAddress, + makerToken, + takerToken, + takerFillAmounts, + ); + case ERC20BridgeSource.MultiBridge: + if (multiBridgeAddress === undefined) { + throw new Error( + 'Cannot sample liquidity from MultiBridge if an address is not provided.', + ); + } + const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); + return this.getMultiBridgeSellQuotes( + multiBridgeAddress, + makerToken, + intermediateToken, + takerToken, + takerFillAmounts, + ); + case ERC20BridgeSource.MStable: + return this.getMStableSellQuotes(makerToken, takerToken, takerFillAmounts); + case ERC20BridgeSource.Mooniswap: + return this.getMooniswapSellQuotes(makerToken, takerToken, takerFillAmounts); + case ERC20BridgeSource.Balancer: + return this.balancerPoolsCache + .getCachedPoolAddressesForPair(takerToken, makerToken)! + .map(poolAddress => + this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts), + ); + default: + throw new Error(`Unsupported sell sample source: ${source}`); + } + }, + ), + ); + } + + private _getBuyQuoteOperations( + sources: ERC20BridgeSource[], + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + wethAddress: string, + liquidityProviderRegistryAddress?: string, + ): SourceQuoteOperation[] { + return _.flatten( + sources.map( + (source): SourceQuoteOperation | SourceQuoteOperation[] => { + switch (source) { + case ERC20BridgeSource.Eth2Dai: + return this.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts); + case ERC20BridgeSource.Uniswap: + return this.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); + case ERC20BridgeSource.UniswapV2: + const ops = [this.getUniswapV2BuyQuotes([takerToken, makerToken], makerFillAmounts)]; + if (takerToken !== wethAddress && makerToken !== wethAddress) { + ops.push( + this.getUniswapV2BuyQuotes([takerToken, wethAddress, makerToken], makerFillAmounts), + ); + } + return ops; + case ERC20BridgeSource.Kyber: + return this.getKyberBuyQuotes(makerToken, takerToken, makerFillAmounts); + case ERC20BridgeSource.Curve: + return getCurveInfosForPair(takerToken, makerToken).map(curve => + this.getCurveBuyQuotes( + curve, + curve.tokens.indexOf(takerToken), + curve.tokens.indexOf(makerToken), + makerFillAmounts, + ), + ); + case ERC20BridgeSource.LiquidityProvider: + if (liquidityProviderRegistryAddress === undefined) { + throw new Error( + 'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', + ); + } + return this.getLiquidityProviderBuyQuotes( + liquidityProviderRegistryAddress, + makerToken, + takerToken, + makerFillAmounts, + ); + case ERC20BridgeSource.MStable: + return this.getMStableBuyQuotes(makerToken, takerToken, makerFillAmounts); + case ERC20BridgeSource.Mooniswap: + return this.getMooniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); + case ERC20BridgeSource.Balancer: + return this.balancerPoolsCache + .getCachedPoolAddressesForPair(takerToken, makerToken)! + .map(poolAddress => + this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts), + ); + default: + throw new Error(`Unsupported buy sample source: ${source}`); + } + }, + ), + ); + } +} // tslint:disable max-file-line-count 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 7db7c5514f..0f98dbd704 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { QuoteRequestor } from '../../utils/quote_requestor'; -import { ERC20BridgeSamplerContract } from '../../wrappers'; import { QuoteReport } from '../quote_report_generator'; /** @@ -41,6 +40,7 @@ export enum ERC20BridgeSource { Bancor = 'Bancor', MStable = 'mStable', Mooniswap = 'Mooniswap', + MultiHop = 'MultiHop', } // tslint:disable: enum-naming @@ -72,6 +72,11 @@ export interface CurveInfo { // Internal `fillData` field for `Fill` objects. export interface FillData {} +export interface SourceInfo { + source: ERC20BridgeSource; + fillData?: TFillData; +} + // `FillData` for native fills. export interface NativeFillData extends FillData { order: SignedOrderWithFillableAmounts; @@ -108,14 +113,23 @@ export interface Quote { fillData?: TFillData; } +export interface HopInfo { + sourceIndex: BigNumber; + returnData: string; +} + +export interface MultiHopFillData extends FillData { + firstHopSource: SourceQuoteOperation; + secondHopSource: SourceQuoteOperation; + intermediateToken: string; +} + /** * Represents an individual DEX sample from the sampler contract. */ -export interface DexSample { - source: ERC20BridgeSource; +export interface DexSample extends SourceInfo { input: BigNumber; output: BigNumber; - fillData?: TFillData; } /** @@ -131,7 +145,7 @@ export enum FillFlags { /** * Represents a node on a fill path. */ -export interface Fill { +export interface Fill extends SourceInfo { // 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). @@ -148,25 +162,16 @@ export interface Fill { parent?: Fill; // The index of the fill in the original path. index: number; - // The source of the fill. See `ERC20BridgeSource`. - source: ERC20BridgeSource; - // Data associated with this this Fill object. Used to reconstruct orders - // from paths. - fillData?: TFillData; } /** * Represents continguous fills on a path that have been merged together. */ -export interface CollapsedFill { +export interface CollapsedFill extends SourceInfo { // 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. - */ - source: ERC20BridgeSource; /** * Total input amount (sum of `subFill`s) */ @@ -182,8 +187,6 @@ export interface CollapsedFill { input: BigNumber; output: BigNumber; }>; - - fillData?: TFillData; } /** @@ -273,17 +276,19 @@ export interface GetMarketOrdersOpts { * A composable operation the be run in `DexOrderSampler.executeAsync()`. */ export interface BatchedOperation { - encodeCall(contract: ERC20BridgeSamplerContract): string; - handleCallResultsAsync(contract: ERC20BridgeSamplerContract, callResults: string): Promise; + encodeCall(): string; + handleCallResults(callResults: string): TResult; } export interface SourceQuoteOperation - extends BatchedOperation>> { - source: ERC20BridgeSource; + extends BatchedOperation, + SourceInfo { + readonly source: ERC20BridgeSource; } -export interface OptimizedOrdersAndQuoteReport { +export interface OptimizerResult { optimizedOrders: OptimizedMarketOrder[]; + isTwoHop: boolean; quoteReport: QuoteReport; } @@ -305,4 +310,9 @@ export interface MarketSideLiquidity { ethToOutputRate: BigNumber; ethToInputRate: BigNumber; rfqtIndicativeQuotes: RFQTIndicativeQuote[]; + twoHopQuotes: Array>; +} + +export interface TokenAdjacencyGraph { + [token: string]: string[]; } diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts index 3ea6f45e8a..d4f870cdb9 100644 --- a/packages/asset-swapper/src/utils/quote_report_generator.ts +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -1,11 +1,17 @@ import { orderHashUtils } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; 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 { + CollapsedFill, + DexSample, + ERC20BridgeSource, + MultiHopFillData, + NativeCollapsedFill, +} from './market_operation_utils/types'; import { QuoteRequestor } from './quote_requestor'; export interface BridgeReportSource { @@ -14,6 +20,13 @@ export interface BridgeReportSource { takerAmount: BigNumber; } +export interface MultiHopReportSource { + liquiditySource: ERC20BridgeSource.MultiHop; + makerAmount: BigNumber; + takerAmount: BigNumber; + hopSources: ERC20BridgeSource[]; +} + interface NativeReportSourceBase { liquiditySource: ERC20BridgeSource.Native; makerAmount: BigNumber; @@ -29,7 +42,11 @@ export interface NativeRFQTReportSource extends NativeReportSourceBase { isRfqt: true; makerUri: string; } -export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource; +export type QuoteReportSource = + | BridgeReportSource + | NativeOrderbookReportSource + | NativeRFQTReportSource + | MultiHopReportSource; export interface QuoteReport { sourcesConsidered: QuoteReportSource[]; @@ -47,115 +64,152 @@ const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefine } }; -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; +/** + * Generates a report of sources considered while computing the optimized + * swap quote, and the sources ultimately included in the computed quote. + */ +export function generateQuoteReport( + marketOperation: MarketOperation, + dexQuotes: DexSample[], + multiHopQuotes: Array>, + nativeOrders: SignedOrder[], + orderFillableAmounts: BigNumber[], + liquidityDelivered: CollapsedFill[] | DexSample, + quoteRequestor?: QuoteRequestor, +): QuoteReport { + // convert order fillable amount array to easy to look up hash + if (orderFillableAmounts.length !== nativeOrders.length) { + // length mismatch, abort + throw new Error('orderFillableAmounts must be the same length as nativeOrders'); } + const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {}; + nativeOrders.forEach((nativeOrder, idx) => { + orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx]; + }); - public generateReport(): QuoteReport { - const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq)); - const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no)); + const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation)); + const nativeOrderSourcesConsidered = nativeOrders.map(order => + _nativeOrderToReportSource( + order, + orderHashesToFillableAmounts[orderHashUtils.getOrderHash(order)], + quoteRequestor, + ), + ); + const multiHopSourcesConsidered = multiHopQuotes.map(quote => + _multiHopSampleToReportSource(quote, marketOperation), + ); + const sourcesConsidered = [ + ...dexReportSourcesConsidered, + ...nativeOrderSourcesConsidered, + ...multiHopSourcesConsidered, + ]; - const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered]; - const sourcesDelivered = this._collapsedFills.map(collapsedFill => { + let sourcesDelivered; + if (Array.isArray(liquidityDelivered)) { + sourcesDelivered = liquidityDelivered.map(collapsedFill => { const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill); if (foundNativeOrder) { - return this._nativeOrderToReportSource(foundNativeOrder); + return _nativeOrderToReportSource( + foundNativeOrder, + orderHashesToFillableAmounts[orderHashUtils.getOrderHash(foundNativeOrder)], + quoteRequestor, + ); } else { - return this._dexSampleToReportSource(collapsedFill); + return _dexSampleToReportSource(collapsedFill, marketOperation); } }); + } else { + sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)]; + } + return { + sourcesConsidered, + sourcesDelivered, + }; +} +function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): 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 (marketOperation === MarketOperation.Buy) { return { - sourcesConsidered, - sourcesDelivered, + makerAmount: ds.input, + takerAmount: ds.output, + liquiditySource, }; - } - - 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, + } else if (marketOperation === MarketOperation.Sell) { + return { + makerAmount: ds.output, + takerAmount: ds.input, + liquiditySource, }; - - // 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; - } + } else { + throw new Error(`Unexpected marketOperation ${marketOperation}`); + } +} + +function _multiHopSampleToReportSource( + ds: DexSample, + marketOperation: MarketOperation, +): MultiHopReportSource { + const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!; + // input and output map to different values + // based on the market operation + if (marketOperation === MarketOperation.Buy) { + return { + liquiditySource: ERC20BridgeSource.MultiHop, + makerAmount: ds.input, + takerAmount: ds.output, + hopSources: [firstHop.source, secondHop.source], + }; + } else if (marketOperation === MarketOperation.Sell) { + return { + liquiditySource: ERC20BridgeSource.MultiHop, + makerAmount: ds.output, + takerAmount: ds.input, + hopSources: [firstHop.source, secondHop.source], + }; + } else { + throw new Error(`Unexpected marketOperation ${marketOperation}`); + } +} + +function _nativeOrderToReportSource( + nativeOrder: SignedOrder, + fillableAmount: BigNumber, + quoteRequestor?: QuoteRequestor, +): NativeRFQTReportSource | NativeOrderbookReportSource { + const orderHash = orderHashUtils.getOrderHash(nativeOrder); + + const nativeOrderBase: NativeReportSourceBase = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: nativeOrder.makerAssetAmount, + takerAmount: nativeOrder.takerAssetAmount, + fillableTakerAmount: fillableAmount, + nativeOrder, + orderHash, + }; + + // if we find this is an rfqt order, label it as such and associate makerUri + const foundRfqtMakerUri = quoteRequestor && 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 222f1ad6c8..3143aff02b 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -355,10 +355,8 @@ export class QuoteRequestor { switch (quoteType) { case 'firm': return 'quote'; - break; case 'indicative': return 'price'; - break; default: throw new Error(`Unexpected quote type ${quoteType}`); } diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 6c22008fd5..2e0b2240bb 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -100,10 +100,11 @@ export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, ...quoteInfo.opts, }; + const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier); const result = fillQuoteOrders( createBestCaseFillOrderCalls(quoteInfo), quoteInfo.fillAmount, - quoteInfo.gasPrice.times(opts.protocolFeeMultiplier), + protocolFeePerFillOrder, opts.gasSchedule, ); return fromIntermediateQuoteFillResult(result, quoteInfo); diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 23f406edd8..2d16f48a3e 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -3,6 +3,7 @@ import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; +import { constants } from '../constants'; import { CalculateSwapQuoteOpts, MarketBuySwapQuote, @@ -16,8 +17,14 @@ import { import { MarketOperationUtils } from './market_operation_utils'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; -import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; -import { isSupportedAssetDataInOrders } from './utils'; +import { + ERC20BridgeSource, + FeeSchedule, + FillData, + GetMarketOrdersOpts, + OptimizedMarketOrder, +} from './market_operation_utils/types'; +import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; import { QuoteReport } from './quote_report_generator'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; @@ -121,8 +128,9 @@ 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 optimizedOrders: OptimizedMarketOrder[] | undefined; + let optimizedOrders: OptimizedMarketOrder[]; let quoteReport: QuoteReport | undefined; + let isTwoHop = false; { // Scale fees by gas price. @@ -149,6 +157,7 @@ export class SwapQuoteCalculator { ); optimizedOrders = buyResult.optimizedOrders; quoteReport = buyResult.quoteReport; + isTwoHop = buyResult.isTwoHop; } else { const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( prunedOrders, @@ -157,22 +166,34 @@ export class SwapQuoteCalculator { ); optimizedOrders = sellResult.optimizedOrders; quoteReport = sellResult.quoteReport; + isTwoHop = sellResult.isTwoHop; } } } // assetData information for the result const { makerAssetData, takerAssetData } = prunedOrders[0]; - return createSwapQuote( - makerAssetData, - takerAssetData, - optimizedOrders, - operation, - assetFillAmount, - gasPrice, - opts.gasSchedule, - quoteReport, - ); + return isTwoHop + ? createTwoHopSwapQuote( + makerAssetData, + takerAssetData, + optimizedOrders, + operation, + assetFillAmount, + gasPrice, + opts.gasSchedule, + quoteReport, + ) + : createSwapQuote( + makerAssetData, + takerAssetData, + optimizedOrders, + operation, + assetFillAmount, + gasPrice, + opts.gasSchedule, + quoteReport, + ); } } @@ -211,6 +232,74 @@ function createSwapQuote( sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), orders: optimizedOrders, quoteReport, + isTwoHop: false, + }; + + if (operation === MarketOperation.Buy) { + return { + ...quoteBase, + type: MarketOperation.Buy, + makerAssetFillAmount: assetFillAmount, + }; + } else { + return { + ...quoteBase, + type: MarketOperation.Sell, + takerAssetFillAmount: assetFillAmount, + }; + } +} + +function createTwoHopSwapQuote( + makerAssetData: string, + takerAssetData: string, + optimizedOrders: OptimizedMarketOrder[], + operation: MarketOperation, + assetFillAmount: BigNumber, + gasPrice: BigNumber, + gasSchedule: FeeSchedule, + quoteReport?: QuoteReport, +): SwapQuote { + const [firstHopOrder, secondHopOrder] = optimizedOrders; + const [firstHopFill] = firstHopOrder.fills; + const [secondHopFill] = secondHopOrder.fills; + const gas = new BigNumber( + gasSchedule[ERC20BridgeSource.MultiHop]!({ + firstHopSource: _.pick(firstHopFill, 'source', 'fillData'), + secondHopSource: _.pick(secondHopFill, 'source', 'fillData'), + }), + ).toNumber(); + + const quoteBase = { + takerAssetData, + makerAssetData, + gasPrice, + bestCaseQuoteInfo: { + makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input, + takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, + totalTakerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output, + feeTakerAssetAmount: constants.ZERO_AMOUNT, + protocolFeeInWeiAmount: constants.ZERO_AMOUNT, + gas, + }, + worstCaseQuoteInfo: { + makerAssetAmount: secondHopOrder.makerAssetAmount, + takerAssetAmount: firstHopOrder.takerAssetAmount, + totalTakerAssetAmount: firstHopOrder.takerAssetAmount, + feeTakerAssetAmount: constants.ZERO_AMOUNT, + protocolFeeInWeiAmount: constants.ZERO_AMOUNT, + gas, + }, + sourceBreakdown: { + [ERC20BridgeSource.MultiHop]: { + proportion: new BigNumber(1), + intermediateToken: getTokenFromAssetData(secondHopOrder.takerAssetData), + hops: [firstHopFill.source, secondHopFill.source], + }, + }, + orders: optimizedOrders, + quoteReport, + isTwoHop: true, }; if (operation === MarketOperation.Buy) { @@ -218,14 +307,12 @@ function createSwapQuote( ...quoteBase, type: MarketOperation.Buy, makerAssetFillAmount: assetFillAmount, - quoteReport, }; } else { return { ...quoteBase, type: MarketOperation.Sell, takerAssetFillAmount: assetFillAmount, - quoteReport, }; } } @@ -234,7 +321,7 @@ function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: Big const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); const breakdown: SwapQuoteOrdersBreakdown = {}; Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { - breakdown[source] = fillAmount.div(totalFillAmount); + breakdown[source as keyof SwapQuoteOrdersBreakdown] = fillAmount.div(totalFillAmount); }); return breakdown; } diff --git a/packages/asset-swapper/src/utils/utils.ts b/packages/asset-swapper/src/utils/utils.ts index 4be46b773c..d5d8a9e5e1 100644 --- a/packages/asset-swapper/src/utils/utils.ts +++ b/packages/asset-swapper/src/utils/utils.ts @@ -113,3 +113,12 @@ export function isERC20EquivalentAssetData(assetData: AssetData): assetData is E export function difference(a: T[], b: T[]): T[] { return a.filter(x => b.indexOf(x) === -1); } + +export function getTokenFromAssetData(assetData: string): string { + const data = assetDataUtils.decodeAssetDataOrThrow(assetData); + if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) { + throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`); + } + // tslint:disable-next-line:no-unnecessary-type-assertion + return (data as ERC20AssetData).tokenAddress; +} diff --git a/packages/asset-swapper/test/artifacts.ts b/packages/asset-swapper/test/artifacts.ts index 7e29c82804..a00a3aaa77 100644 --- a/packages/asset-swapper/test/artifacts.ts +++ b/packages/asset-swapper/test/artifacts.ts @@ -6,11 +6,13 @@ import { ContractArtifact } from 'ethereum-types'; import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json'; +import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json'; import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json'; import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json'; import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json'; import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json'; +import * as IBalancer from '../test/generated-artifacts/IBalancer.json'; import * as ICurve from '../test/generated-artifacts/ICurve.json'; import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json'; import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json'; @@ -33,15 +35,27 @@ import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSamp import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json'; +import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json'; import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json'; import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json'; export const artifacts = { ApproximateBuys: ApproximateBuys as ContractArtifact, + BalancerSampler: BalancerSampler as ContractArtifact, CurveSampler: CurveSampler as ContractArtifact, - DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, - DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact, ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, Eth2DaiSampler: Eth2DaiSampler as ContractArtifact, + IMooniswap: IMooniswap as ContractArtifact, + KyberSampler: KyberSampler as ContractArtifact, + LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact, + MStableSampler: MStableSampler as ContractArtifact, + MooniswapSampler: MooniswapSampler as ContractArtifact, + MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, + NativeOrderSampler: NativeOrderSampler as ContractArtifact, + SamplerUtils: SamplerUtils as ContractArtifact, + TwoHopSampler: TwoHopSampler as ContractArtifact, + UniswapSampler: UniswapSampler as ContractArtifact, + UniswapV2Sampler: UniswapV2Sampler as ContractArtifact, + IBalancer: IBalancer as ContractArtifact, ICurve: ICurve as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, IKyberHintHandler: IKyberHintHandler as ContractArtifact, @@ -51,19 +65,11 @@ export const artifacts = { ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, IMStable: IMStable as ContractArtifact, - IMooniswap: IMooniswap as ContractArtifact, IMultiBridge: IMultiBridge as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, - KyberSampler: KyberSampler as ContractArtifact, - LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact, - MStableSampler: MStableSampler as ContractArtifact, - MooniswapSampler: MooniswapSampler as ContractArtifact, - MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, - NativeOrderSampler: NativeOrderSampler as ContractArtifact, - SamplerUtils: SamplerUtils as ContractArtifact, - UniswapSampler: UniswapSampler as ContractArtifact, - UniswapV2Sampler: UniswapV2Sampler as ContractArtifact, + DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, + DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, TestNativeOrderSampler: TestNativeOrderSampler as ContractArtifact, }; diff --git a/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts b/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts index 6a871797d8..535b86b213 100644 --- a/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts +++ b/packages/asset-swapper/test/contracts/erc20_bridge_sampler_test.ts @@ -37,6 +37,7 @@ blockchainTests('erc20-bridge-sampler', env => { const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); + const INTERMEDIATE_TOKEN = randomAddress(); before(async () => { testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync( @@ -262,6 +263,33 @@ blockchainTests('erc20-bridge-sampler', env => { await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 }); } + function expectQuotesWithinRange( + quotes: BigNumber[], + expectedQuotes: BigNumber[], + maxSlippage: BigNumber | number, + ): void { + quotes.forEach((_q, i) => { + // If we're within 1 base unit of a low decimal token + // then that's as good as we're going to get (and slippage is "high") + if ( + expectedQuotes[i].isZero() || + BigNumber.max(expectedQuotes[i], quotes[i]) + .minus(BigNumber.min(expectedQuotes[i], quotes[i])) + .eq(1) + ) { + return; + } + const slippage = quotes[i] + .dividedBy(expectedQuotes[i]) + .minus(1) + .decimalPlaces(4); + expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0); + expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte( + new BigNumber(maxSlippage), + ); + }); + } + describe('getOrderFillableTakerAssetAmounts()', () => { it('returns the expected amount for each order', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); @@ -385,32 +413,6 @@ blockchainTests('erc20-bridge-sampler', env => { const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync(); expect(quotes).to.deep.eq([]); }); - const expectQuotesWithinRange = ( - quotes: BigNumber[], - expectedQuotes: BigNumber[], - maxSlippage: BigNumber | number, - ) => { - quotes.forEach((_q, i) => { - // If we're within 1 base unit of a low decimal token - // then that's as good as we're going to get (and slippage is "high") - if ( - expectedQuotes[i].isZero() || - BigNumber.max(expectedQuotes[i], quotes[i]) - .minus(BigNumber.min(expectedQuotes[i], quotes[i])) - .eq(1) - ) { - return; - } - const slippage = quotes[i] - .dividedBy(expectedQuotes[i]) - .minus(1) - .decimalPlaces(4); - expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0); - expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte( - new BigNumber(maxSlippage), - ); - }); - }; it('can quote token -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); @@ -803,7 +805,7 @@ blockchainTests('erc20-bridge-sampler', env => { }); }); - describe('getLiquidityProviderFromRegistry', () => { + describe('liquidity provider', () => { const xAsset = randomAddress(); const yAsset = randomAddress(); const sampleAmounts = getSampleAmounts(yAsset); @@ -829,42 +831,28 @@ blockchainTests('erc20-bridge-sampler', env => { .awaitTransactionSuccessAsync(); }); - it('should be able to get the liquidity provider', async () => { - const xyLiquidityProvider = await testContract - .getLiquidityProviderFromRegistry(registryContract.address, xAsset, yAsset) - .callAsync(); - const yxLiquidityProvider = await testContract - .getLiquidityProviderFromRegistry(registryContract.address, yAsset, xAsset) - .callAsync(); - const unknownLiquidityProvider = await testContract - .getLiquidityProviderFromRegistry(registryContract.address, yAsset, randomAddress()) - .callAsync(); - - expect(xyLiquidityProvider).to.eq(liquidityProvider.address); - expect(yxLiquidityProvider).to.eq(liquidityProvider.address); - expect(unknownLiquidityProvider).to.eq(constants.NULL_ADDRESS); - }); - it('should be able to query sells from the liquidity provider', async () => { - const result = await testContract + const [quotes, providerAddress] = await testContract .sampleSellsFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts) .callAsync(); - result.forEach((value, idx) => { + quotes.forEach((value, idx) => { expect(value).is.bignumber.eql(sampleAmounts[idx].minus(1)); }); + expect(providerAddress).to.equal(liquidityProvider.address); }); it('should be able to query buys from the liquidity provider', async () => { - const result = await testContract + const [quotes, providerAddress] = await testContract .sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts) .callAsync(); - result.forEach((value, idx) => { + quotes.forEach((value, idx) => { expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1)); }); + expect(providerAddress).to.equal(liquidityProvider.address); }); it('should just return zeros if the liquidity provider cannot be found', async () => { - const result = await testContract + const [quotes, providerAddress] = await testContract .sampleBuysFromLiquidityProviderRegistry( registryContract.address, yAsset, @@ -872,18 +860,20 @@ blockchainTests('erc20-bridge-sampler', env => { sampleAmounts, ) .callAsync(); - result.forEach(value => { + quotes.forEach(value => { expect(value).is.bignumber.eql(constants.ZERO_AMOUNT); }); + expect(providerAddress).to.equal(constants.NULL_ADDRESS); }); it('should just return zeros if the registry does not exist', async () => { - const result = await testContract + const [quotes, providerAddress] = await testContract .sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts) .callAsync(); - result.forEach(value => { + quotes.forEach(value => { expect(value).is.bignumber.eql(constants.ZERO_AMOUNT); }); + expect(providerAddress).to.equal(constants.NULL_ADDRESS); }); }); @@ -1033,4 +1023,116 @@ blockchainTests('erc20-bridge-sampler', env => { ); }); }); + + blockchainTests.resets('TwoHopSampler', () => { + before(async () => { + await testContract + .createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN, INTERMEDIATE_TOKEN]) + .awaitTransactionSuccessAsync(); + }); + + it('sampleTwoHopSell', async () => { + // tslint:disable-next-line no-unnecessary-type-assertion + const sellAmount = _.last(getSampleAmounts(TAKER_TOKEN))!; + const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN]; + const uniswapV2FirstHop = testContract + .sampleSellsFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN]; + const uniswapV2SecondHop = testContract + .sampleSellsFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const eth2DaiFirstHop = testContract + .sampleSellsFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + const eth2DaiSecondHop = testContract + .sampleSellsFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const firstHopQuotes = [ + getDeterministicSellQuote(ETH2DAI_SALT, TAKER_TOKEN, INTERMEDIATE_TOKEN, sellAmount), + getDeterministicUniswapV2SellQuote(uniswapV2FirstHopPath, sellAmount), + ]; + const expectedIntermediateAssetAmount = BigNumber.max(...firstHopQuotes); + const secondHopQuotes = [ + getDeterministicSellQuote( + ETH2DAI_SALT, + INTERMEDIATE_TOKEN, + MAKER_TOKEN, + expectedIntermediateAssetAmount, + ), + getDeterministicUniswapV2SellQuote(uniswapV2SecondHopPath, expectedIntermediateAssetAmount), + ]; + const expectedBuyAmount = BigNumber.max(...secondHopQuotes); + + const [firstHop, secondHop, buyAmount] = await testContract + .sampleTwoHopSell( + [eth2DaiFirstHop, uniswapV2FirstHop], + [eth2DaiSecondHop, uniswapV2SecondHop], + sellAmount, + ) + .callAsync(); + expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal( + firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)), + ); + expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal( + secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedBuyAmount)), + ); + expect(buyAmount, 'Two hop buy amount').to.bignumber.equal(expectedBuyAmount); + }); + it('sampleTwoHopBuy', async () => { + // tslint:disable-next-line no-unnecessary-type-assertion + const buyAmount = _.last(getSampleAmounts(MAKER_TOKEN))!; + const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN]; + const uniswapV2FirstHop = testContract + .sampleBuysFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN]; + const uniswapV2SecondHop = testContract + .sampleBuysFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const eth2DaiFirstHop = testContract + .sampleBuysFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + const eth2DaiSecondHop = testContract + .sampleBuysFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT]) + .getABIEncodedTransactionData(); + + const secondHopQuotes = [ + getDeterministicBuyQuote(ETH2DAI_SALT, INTERMEDIATE_TOKEN, MAKER_TOKEN, buyAmount), + getDeterministicUniswapV2BuyQuote(uniswapV2SecondHopPath, buyAmount), + ]; + const expectedIntermediateAssetAmount = BigNumber.min(...secondHopQuotes); + + const firstHopQuotes = [ + getDeterministicBuyQuote( + ETH2DAI_SALT, + TAKER_TOKEN, + INTERMEDIATE_TOKEN, + expectedIntermediateAssetAmount, + ), + getDeterministicUniswapV2BuyQuote(uniswapV2FirstHopPath, expectedIntermediateAssetAmount), + ]; + const expectedSellAmount = BigNumber.min(...firstHopQuotes); + + const [firstHop, secondHop, sellAmount] = await testContract + .sampleTwoHopBuy( + [eth2DaiFirstHop, uniswapV2FirstHop], + [eth2DaiSecondHop, uniswapV2SecondHop], + buyAmount, + ) + .callAsync(); + expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal( + firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedSellAmount)), + ); + expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal( + secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)), + ); + expect(sellAmount, 'Two hop sell amount').to.bignumber.equal(expectedSellAmount); + }); + }); }); diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts index 9bc5465235..7aecbf8232 100644 --- a/packages/asset-swapper/test/dex_sampler_test.ts +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -18,7 +18,7 @@ import { computeBalancerSellQuote, } from '../src/utils/market_operation_utils/balancer_utils'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; -import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types'; +import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache'; import { MockBancorService } from './utils/mock_bancor_service'; @@ -108,7 +108,7 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), ); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); }); @@ -124,7 +124,7 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), ); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); }); @@ -144,36 +144,32 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getKyberSellQuotes( - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - ), + dexOrderSampler.getKyberSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); }); it('getLiquidityProviderSellQuotes()', async () => { const expectedMakerToken = randomAddress(); const expectedTakerToken = randomAddress(); const registry = randomAddress(); + const poolAddress = randomAddress(); const sampler = new MockSamplerContract({ sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { expect(registryAddress).to.eq(registry); expect(takerToken).to.eq(expectedTakerToken); expect(makerToken).to.eq(expectedMakerToken); - return [toBaseUnitAmount(1001)]; + return [[toBaseUnitAmount(1001)], poolAddress]; }, }); const dexOrderSampler = new DexOrderSampler(sampler); const [result] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getSellQuotesAsync( + dexOrderSampler.getSellQuotes( [ERC20BridgeSource.LiquidityProvider], expectedMakerToken, expectedTakerToken, [toBaseUnitAmount(1000)], wethAddress, - dexOrderSampler.balancerPoolsCache, registry, ), ); @@ -183,7 +179,7 @@ describe('DexSampler tests', () => { source: 'LiquidityProvider', output: toBaseUnitAmount(1001), input: toBaseUnitAmount(1000), - fillData: undefined, + fillData: { poolAddress }, }, ], ]); @@ -193,23 +189,23 @@ describe('DexSampler tests', () => { const expectedMakerToken = randomAddress(); const expectedTakerToken = randomAddress(); const registry = randomAddress(); + const poolAddress = randomAddress(); const sampler = new MockSamplerContract({ sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { expect(registryAddress).to.eq(registry); expect(takerToken).to.eq(expectedTakerToken); expect(makerToken).to.eq(expectedMakerToken); - return [toBaseUnitAmount(999)]; + return [[toBaseUnitAmount(999)], poolAddress]; }, }); const dexOrderSampler = new DexOrderSampler(sampler); const [result] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getBuyQuotesAsync( + dexOrderSampler.getBuyQuotes( [ERC20BridgeSource.LiquidityProvider], expectedMakerToken, expectedTakerToken, [toBaseUnitAmount(1000)], wethAddress, - dexOrderSampler.balancerPoolsCache, registry, ), ); @@ -219,7 +215,7 @@ describe('DexSampler tests', () => { source: 'LiquidityProvider', output: toBaseUnitAmount(999), input: toBaseUnitAmount(1000), - fillData: undefined, + fillData: { poolAddress }, }, ], ]); @@ -246,13 +242,12 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [result] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getSellQuotesAsync( + dexOrderSampler.getSellQuotes( [ERC20BridgeSource.MultiBridge], expectedMakerToken, expectedTakerToken, [toBaseUnitAmount(1000)], randomAddress(), - dexOrderSampler.balancerPoolsCache, randomAddress(), multiBridge, ), @@ -263,7 +258,7 @@ describe('DexSampler tests', () => { source: 'MultiBridge', output: toBaseUnitAmount(1001), input: toBaseUnitAmount(1000), - fillData: undefined, + fillData: { poolAddress: multiBridge }, }, ], ]); @@ -284,13 +279,9 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getEth2DaiSellQuotes( - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - ), + dexOrderSampler.getEth2DaiSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); }); it('getUniswapSellQuotes()', async () => { @@ -308,13 +299,9 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getUniswapSellQuotes( - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - ), + dexOrderSampler.getUniswapSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); }); it('getUniswapV2SellQuotes()', async () => { @@ -331,12 +318,12 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getUniswapV2SellQuotes( + dexOrderSampler.getUniswapV2SellQuotes( [expectedMakerToken, expectedTakerToken], expectedTakerFillAmounts, ), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts); }); it('getEth2DaiBuyQuotes()', async () => { @@ -354,13 +341,9 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getEth2DaiBuyQuotes( - expectedMakerToken, - expectedTakerToken, - expectedMakerFillAmounts, - ), + dexOrderSampler.getEth2DaiBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts); }); it('getUniswapBuyQuotes()', async () => { @@ -378,13 +361,9 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getUniswapBuyQuotes( - expectedMakerToken, - expectedTakerToken, - expectedMakerFillAmounts, - ), + dexOrderSampler.getUniswapBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts), ); - expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts); + expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts); }); interface RatesBySource { @@ -440,13 +419,12 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [quotes] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getSellQuotesAsync( + dexOrderSampler.getSellQuotes( sources, expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts, wethAddress, - dexOrderSampler.balancerPoolsCache, ), ); const expectedQuotes = sources.map(s => @@ -457,7 +435,7 @@ describe('DexSampler tests', () => { fillData: s === ERC20BridgeSource.UniswapV2 ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } - : ((undefined as any) as FillData), + : {}, })), ); const uniswapV2ETHQuotes = [ @@ -488,19 +466,14 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler( new MockSamplerContract({}), - undefined, // sampler overrides - undefined, // bancor service + undefined, + undefined, balancerPoolsCache, ); - const [quotes] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getSellQuotesAsync( - [ERC20BridgeSource.Balancer], - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - wethAddress, - dexOrderSampler.balancerPoolsCache, - ), + const quotes = await dexOrderSampler.getBalancerSellQuotesOffChainAsync( + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, ); const expectedQuotes = pools.map(p => expectedTakerFillAmounts.map(a => ({ @@ -535,28 +508,17 @@ describe('DexSampler tests', () => { bancorService, undefined, // balancer cache ); - const [quotes] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getSellQuotesAsync( - [ERC20BridgeSource.Bancor], - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - wethAddress, - undefined, // balancer pools cache - undefined, // liquidity provider registry address - undefined, // multibridge address - bancorService, - ), + const quotes = await dexOrderSampler.getBancorSellQuotesOffChainAsync( + expectedMakerToken, + expectedTakerToken, + expectedTakerFillAmounts, ); - const expectedQuotes = [ - expectedTakerFillAmounts.map(a => ({ - source: ERC20BridgeSource.Bancor, - input: a, - output: a.multipliedBy(rate), - fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress }, - })), - ]; - expect(quotes).to.have.lengthOf(1); // one set per pool + const expectedQuotes = expectedTakerFillAmounts.map(a => ({ + source: ERC20BridgeSource.Bancor, + input: a, + output: a.multipliedBy(rate), + fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress }, + })); expect(quotes).to.deep.eq(expectedQuotes); }); it('getBuyQuotes()', async () => { @@ -596,13 +558,12 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [quotes] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getBuyQuotesAsync( + dexOrderSampler.getBuyQuotes( sources, expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts, wethAddress, - dexOrderSampler.balancerPoolsCache, ), ); const expectedQuotes = sources.map(s => @@ -613,7 +574,7 @@ describe('DexSampler tests', () => { fillData: s === ERC20BridgeSource.UniswapV2 ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } - : ((undefined as any) as FillData), + : {}, })), ); const uniswapV2ETHQuotes = [ @@ -644,19 +605,14 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler( new MockSamplerContract({}), - undefined, // sampler overrides - undefined, // bancor service + undefined, + undefined, balancerPoolsCache, ); - const [quotes] = await dexOrderSampler.executeAsync( - await DexOrderSampler.ops.getBuyQuotesAsync( - [ERC20BridgeSource.Balancer], - expectedMakerToken, - expectedTakerToken, - expectedMakerFillAmounts, - wethAddress, - dexOrderSampler.balancerPoolsCache, - ), + const quotes = await dexOrderSampler.getBalancerBuyQuotesOffChainAsync( + expectedMakerToken, + expectedTakerToken, + expectedMakerFillAmounts, ); const expectedQuotes = pools.map(p => expectedMakerFillAmounts.map(a => ({ @@ -689,8 +645,8 @@ describe('DexSampler tests', () => { }); const dexOrderSampler = new DexOrderSampler(sampler); const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( - DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), - DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), + dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), ); expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); 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 03052506fc..7692eefae8 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 @@ -37,6 +37,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { const CHAIN_ID = 1; const TAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress(); + const INTERMEDIATE_TOKEN = randomAddress(); const TRANSFORMER_DEPLOYER = randomAddress(); const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), @@ -124,6 +125,18 @@ describe('ExchangeProxySwapQuoteConsumer', () => { } as any; } + function getRandomTwoHopQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote { + const intermediateTokenAssetData = createAssetData(INTERMEDIATE_TOKEN); + return { + ...getRandomQuote(side), + orders: [ + { ...getRandomOrder(), makerAssetData: intermediateTokenAssetData }, + { ...getRandomOrder(), takerAssetData: intermediateTokenAssetData }, + ], + isTwoHop: true, + } as any; + } + function getRandomSellQuote(): MarketSellSwapQuote { return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote; } @@ -298,5 +311,52 @@ describe('ExchangeProxySwapQuoteConsumer', () => { }), ).to.eventually.be.rejectedWith('Affiliate fees denominated in sell token are not yet supported'); }); + it('Uses two `FillQuoteTransformer`s if given two-hop sell quote', async () => { + const quote = getRandomTwoHopQuote(MarketOperation.Sell) as MarketSellSwapQuote; + const callInfo = await consumer.getCalldataOrThrowAsync(quote, { + extensionContractOpts: { isTwoHop: true }, + }); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + expect(callArgs.inputToken).to.eq(TAKER_TOKEN); + expect(callArgs.outputToken).to.eq(MAKER_TOKEN); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount); + expect(callArgs.transformations).to.be.length(3); + expect( + callArgs.transformations[0].deploymentNonce.toNumber() === + consumer.transformerNonces.fillQuoteTransformer, + ); + expect( + callArgs.transformations[1].deploymentNonce.toNumber() === + consumer.transformerNonces.fillQuoteTransformer, + ); + expect( + callArgs.transformations[2].deploymentNonce.toNumber() === + consumer.transformerNonces.payTakerTransformer, + ); + const [firstHopOrder, secondHopOrder] = quote.orders; + const firstHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); + expect(firstHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); + expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAssetAmount); + expect(firstHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([firstHopOrder])); + expect(firstHopFillQuoteTransformerData.signatures).to.deep.eq([firstHopOrder.signature]); + expect(firstHopFillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); + expect(firstHopFillQuoteTransformerData.buyToken).to.eq(INTERMEDIATE_TOKEN); + const secondHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[1].data); + expect(secondHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); + expect(secondHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(contractConstants.MAX_UINT256); + expect(secondHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([secondHopOrder])); + expect(secondHopFillQuoteTransformerData.signatures).to.deep.eq([secondHopOrder.signature]); + expect(secondHopFillQuoteTransformerData.sellToken).to.eq(INTERMEDIATE_TOKEN); + expect(secondHopFillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); + const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[2].data); + expect(payTakerTransformerData.amounts).to.deep.eq([]); + expect(payTakerTransformerData.tokens).to.deep.eq([ + TAKER_TOKEN, + MAKER_TOKEN, + ETH_TOKEN_ADDRESS, + INTERMEDIATE_TOKEN, + ]); + }); }); }); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 7d23096ce9..610c1b8562 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -17,6 +17,7 @@ import * as TypeMoq from 'typemoq'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; +import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils'; import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants'; import { createFillPaths } from '../src/utils/market_operation_utils/fills'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; @@ -31,15 +32,6 @@ const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); describe('MarketOperationUtils tests', () => { const CHAIN_ID = 1; const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS }; - let originalSamplerOperations: any; - - before(() => { - originalSamplerOperations = DexOrderSampler.ops; - }); - - after(() => { - DexOrderSampler.ops = originalSamplerOperations; - }); function createOrder(overrides?: Partial): SignedOrder { return { @@ -153,7 +145,7 @@ describe('MarketOperationUtils tests', () => { fillAmounts: BigNumber[], wethAddress: string, liquidityProviderAddress?: string, - ) => Promise; + ) => DexSample[][]; function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { return ( @@ -163,7 +155,7 @@ describe('MarketOperationUtils tests', () => { fillAmounts: BigNumber[], _wethAddress: string, ) => { - return Promise.resolve(sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]))); + return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s])); }; } @@ -181,7 +173,6 @@ describe('MarketOperationUtils tests', () => { takerToken: string, fillAmounts: BigNumber[], wethAddress: string, - _balancerPoolsCache?: any, liquidityProviderAddress?: string, ) => { liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress; @@ -206,9 +197,7 @@ describe('MarketOperationUtils tests', () => { fillAmounts: BigNumber[], _wethAddress: string, ) => { - return Promise.resolve( - sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))), - ); + return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))); }; } @@ -221,12 +210,6 @@ describe('MarketOperationUtils tests', () => { liquidityProviderAddress?: string, ) => BigNumber; - type GetLiquidityProviderFromRegistryOperation = ( - registryAddress: string, - takerToken: string, - makerToken: string, - ) => string; - function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { return ( _sources: ERC20BridgeSource[], @@ -239,34 +222,6 @@ describe('MarketOperationUtils tests', () => { }; } - function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation { - return (_registryAddress: string, _takerToken: string, _makerToken: string): string => { - return NULL_ADDRESS; - }; - } - - function getLiquidityProviderFromRegistryAndReturnCallParameters( - liquidityProviderAddress: string = NULL_ADDRESS, - ): [ - { registryAddress?: string; takerToken?: string; makerToken?: string }, - GetLiquidityProviderFromRegistryOperation - ] { - const callArgs: { registryAddress?: string; takerToken?: string; makerToken?: string } = { - registryAddress: undefined, - takerToken: undefined, - makerToken: undefined, - }; - const fn = (registryAddress: string, takerToken: string, makerToken: string): string => { - callArgs.makerToken = makerToken; - callArgs.takerToken = takerToken; - if (registryAddress !== constants.NULL_ADDRESS) { - callArgs.registryAddress = registryAddress; - } - return liquidityProviderAddress; - }; - return [callArgs, fn]; - } - function createDecreasingRates(count: number): BigNumber[] { const rates: BigNumber[] = []; const initialRate = getRandomFloat(1e-3, 1e2); @@ -317,6 +272,7 @@ describe('MarketOperationUtils tests', () => { fromTokenIdx: 0, toTokenIdx: 1, }, + [ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() }, }; const DEFAULT_OPS = { @@ -326,19 +282,44 @@ describe('MarketOperationUtils tests', () => { getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { return orders.map(o => o.makerAssetAmount); }, - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), - getMedianSellRateAsync: createGetMedianSellRate(1), - getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), + getMedianSellRate: createGetMedianSellRate(1), + getBalancerSellQuotesOffChainAsync: ( + _makerToken: string, + _takerToken: string, + takerFillAmounts: BigNumber[], + ) => [ + createSamplesFromRates( + ERC20BridgeSource.Balancer, + takerFillAmounts, + createDecreasingRates(takerFillAmounts.length), + DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer], + ), + ], + getBalancerBuyQuotesOffChainAsync: ( + _makerToken: string, + _takerToken: string, + makerFillAmounts: BigNumber[], + ) => [ + createSamplesFromRates( + ERC20BridgeSource.Balancer, + makerFillAmounts, + createDecreasingRates(makerFillAmounts.length).map(r => new BigNumber(1).div(r)), + DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer], + ), + ], + getBancorSellQuotesOffChainAsync: (_makerToken: string, _takerToken: string, takerFillAmounts: BigNumber[]) => + createSamplesFromRates( + ERC20BridgeSource.Bancor, + takerFillAmounts, + createDecreasingRates(takerFillAmounts.length), + DEFAULT_FILL_DATA[ERC20BridgeSource.Bancor], + ), + getTwoHopSellQuotes: (..._params: any[]) => [], + getTwoHopBuyQuotes: (..._params: any[]) => [], }; - function replaceSamplerOps(ops: Partial = {}): void { - DexOrderSampler.ops = { - ...DEFAULT_OPS, - ...ops, - } as any; - } - const MOCK_SAMPLER = ({ async executeAsync(...ops: any[]): Promise { return ops; @@ -346,8 +327,14 @@ describe('MarketOperationUtils tests', () => { async executeBatchAsync(ops: any[]): Promise { return ops; }, + balancerPoolsCache: new BalancerPoolsCache(), } as any) as DexOrderSampler; + function replaceSamplerOps(ops: Partial = {}): void { + Object.assign(MOCK_SAMPLER, DEFAULT_OPS); + Object.assign(MOCK_SAMPLER, ops); + } + describe('getRfqtIndicativeQuotesAsync', () => { const partialRfqt: RfqtRequestOpts = { apiKey: 'foo', @@ -434,6 +421,7 @@ describe('MarketOperationUtils tests', () => { ERC20BridgeSource.Balancer, ERC20BridgeSource.MStable, ERC20BridgeSource.Mooniswap, + ERC20BridgeSource.Bancor, ], allowFallback: false, shouldBatchBridgeOrders: false, @@ -447,9 +435,9 @@ describe('MarketOperationUtils tests', () => { const numSamples = _.random(1, NUM_SAMPLES); let actualNumSamples = 0; replaceSamplerOps({ - getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { actualNumSamples = amounts.length; - return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -462,9 +450,17 @@ describe('MarketOperationUtils tests', () => { it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ - getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); - return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); + }, + getBalancerSellQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ) => { + sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -480,7 +476,15 @@ describe('MarketOperationUtils tests', () => { DEFAULT_RATES, ); replaceSamplerOps({ - getSellQuotesAsync: fn, + getSellQuotes: fn, + getBalancerSellQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ) => { + args.sources = args.sources.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); + }, }); const registryAddress = randomAddress(); const newMarketOperationUtils = new MarketOperationUtils( @@ -503,9 +507,17 @@ describe('MarketOperationUtils tests', () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ - getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); - return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); + }, + getBalancerSellQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ) => { + sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); }, }); await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -576,7 +588,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -614,8 +626,8 @@ describe('MarketOperationUtils tests', () => { ), }; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -652,8 +664,8 @@ describe('MarketOperationUtils tests', () => { ), }; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -678,8 +690,8 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1], }; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -703,7 +715,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -724,7 +736,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -741,7 +753,8 @@ describe('MarketOperationUtils tests', () => { it('is able to create a order from LiquidityProvider', async () => { const registryAddress = randomAddress(); - const liquidityProviderAddress = randomAddress(); + const liquidityProviderAddress = (DEFAULT_FILL_DATA[ERC20BridgeSource.LiquidityProvider] as any) + .poolAddress; const xAsset = randomAddress(); const yAsset = randomAddress(); const toSell = fromTokenUnitAmount(10); @@ -752,14 +765,10 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.LiquidityProvider]: createDecreasingRates(5), }, ); - const [ - getLiquidityProviderParams, - getLiquidityProviderFn, - ] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress); + replaceSamplerOps({ getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], - getSellQuotesAsync: getSellQuotesFn, - getLiquidityProviderFromRegistry: getLiquidityProviderFn, + getSellQuotes: getSellQuotesFn, }); const sampler = new MarketOperationUtils( @@ -776,7 +785,12 @@ describe('MarketOperationUtils tests', () => { }), ], Web3Wrapper.toBaseUnitAmount(10, 18), - { excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false }, + { + excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor), + numSamples: 4, + bridgeSlippage: 0, + shouldBatchBridgeOrders: false, + }, ); const result = ordersAndReport.optimizedOrders; expect(result.length).to.eql(1); @@ -791,9 +805,6 @@ describe('MarketOperationUtils tests', () => { expect(result[0].takerAssetAmount).to.bignumber.eql(toSell); expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider); expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); - expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress); - expect(getLiquidityProviderParams.makerToken).is.eql(yAsset); - expect(getLiquidityProviderParams.takerToken).is.eql(xAsset); }); it('batches contiguous bridge sources', async () => { @@ -803,7 +814,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; replaceSamplerOps({ - getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -860,9 +871,9 @@ describe('MarketOperationUtils tests', () => { const numSamples = _.random(1, 16); let actualNumSamples = 0; replaceSamplerOps({ - getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { actualNumSamples = amounts.length; - return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -875,9 +886,17 @@ describe('MarketOperationUtils tests', () => { it('polls all DEXes if `excludedSources` is empty', async () => { let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ - getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); - return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); + }, + getBalancerBuyQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ) => { + sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -893,7 +912,15 @@ describe('MarketOperationUtils tests', () => { DEFAULT_RATES, ); replaceSamplerOps({ - getBuyQuotesAsync: fn, + getBuyQuotes: fn, + getBalancerBuyQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ) => { + args.sources = args.sources.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); + }, }); const registryAddress = randomAddress(); const newMarketOperationUtils = new MarketOperationUtils( @@ -916,9 +943,17 @@ describe('MarketOperationUtils tests', () => { const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); let sourcesPolled: ERC20BridgeSource[] = []; replaceSamplerOps({ - getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { + getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { sourcesPolled = sourcesPolled.concat(sources.slice()); - return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); + return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); + }, + getBalancerBuyQuotesOffChainAsync: ( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ) => { + sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); + return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); }, }); await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { @@ -988,7 +1023,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; replaceSamplerOps({ - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -1026,8 +1061,8 @@ describe('MarketOperationUtils tests', () => { ), }; replaceSamplerOps({ - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), - getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -1063,8 +1098,8 @@ describe('MarketOperationUtils tests', () => { ), }; replaceSamplerOps({ - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), - getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -1087,7 +1122,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; replaceSamplerOps({ - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -1107,7 +1142,7 @@ describe('MarketOperationUtils tests', () => { rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; replaceSamplerOps({ - getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), @@ -1128,7 +1163,7 @@ describe('MarketOperationUtils tests', () => { 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), + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), }); const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), diff --git a/packages/asset-swapper/test/quote_report_generator_test.ts b/packages/asset-swapper/test/quote_report_generator_test.ts index e90b19eaf5..d1d1ccdd63 100644 --- a/packages/asset-swapper/test/quote_report_generator_test.ts +++ b/packages/asset-swapper/test/quote_report_generator_test.ts @@ -12,15 +12,17 @@ import { CollapsedFill, DexSample, ERC20BridgeSource, + MultiHopFillData, NativeCollapsedFill, } from '../src/utils/market_operation_utils/types'; import { QuoteRequestor } from '../src/utils/quote_requestor'; import { BridgeReportSource, + generateQuoteReport, + MultiHopReportSource, NativeOrderbookReportSource, NativeRFQTReportSource, - QuoteReportGenerator, QuoteReportSource, } from './../src/utils/quote_report_generator'; import { chaiSetup } from './utils/chai_setup'; @@ -47,309 +49,378 @@ const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill = }; }; -describe('QuoteReportGenerator', async () => { - describe('generateReport', async () => { - it('should generate report properly for sell', () => { - const marketOperation: MarketOperation = MarketOperation.Sell; +describe('generateQuoteReport', 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 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(); + const orderbookOrder1FillableAmount = new BigNumber(1000); + const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder1', + takerAssetAmount: orderbookOrder1FillableAmount, }); - 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 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, + ]; - 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 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]; - // 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]; + // 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, - ).generateReport(); + const orderReport = generateQuoteReport( + marketOperation, + dexQuotes, + [], + nativeOrders, + orderFillableAmounts, + pathGenerated, + quoteRequestor.object, + ); - 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 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, - 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 expectedSourcesConsidered: QuoteReportSource[] = [ + kyber1Source, + kyber2Source, + uniswap1Source, + uniswap2Source, + orderbookOrder1Source, + rfqtOrder1Source, + rfqtOrder2Source, + orderbookOrder2Source, + ]; - const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; - expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length); - orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => { - const expectedSourceDelivered = expectedSourcesDelivered[idx]; + expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); - // remove fillable values - if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { - actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ - 'fillableMakerAssetAmount', - 'fillableTakerAssetAmount', - 'fillableTakerFeeAmount', - ]) as SignedOrder; - } + orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { + const expectedSourceConsidered = expectedSourcesConsidered[idx]; + expect(actualSourcesConsidered).to.eql( + expectedSourceConsidered, + `sourceConsidered incorrect at index ${idx}`, + ); + }); - expect(actualSourceDelivered).to.eql( - expectedSourceDelivered, - `sourceDelivered 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 = generateQuoteReport( + marketOperation, + dexQuotes, + [], + nativeOrders, + orderFillableAmounts, + pathGenerated, + ); + + 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}`); }); }); + it('should correctly generate report for a two-hop quote', () => { + const marketOperation: MarketOperation = MarketOperation.Sell; + const kyberSample1: DexSample = { + source: ERC20BridgeSource.Kyber, + input: new BigNumber(10000), + output: new BigNumber(10001), + }; + + const orderbookOrder1FillableAmount = new BigNumber(1000); + const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ + signature: 'orderbookOrder1', + takerAssetAmount: orderbookOrder1FillableAmount.plus(101), + }); + + const twoHopSample: DexSample = { + source: ERC20BridgeSource.MultiHop, + input: new BigNumber(3005), + output: new BigNumber(3006), + fillData: { + intermediateToken: hexUtils.random(20), + firstHopSource: { + source: ERC20BridgeSource.Balancer, + encodeCall: () => '', + handleCallResults: _callResults => [new BigNumber(1337)], + }, + secondHopSource: { + source: ERC20BridgeSource.Curve, + encodeCall: () => '', + handleCallResults: _callResults => [new BigNumber(1337)], + }, + }, + }; + + const orderReport = generateQuoteReport( + marketOperation, + [kyberSample1], + [twoHopSample], + [orderbookOrder1], + [orderbookOrder1FillableAmount], + twoHopSample, + ); + const orderbookOrder1Source: NativeOrderbookReportSource = { + liquiditySource: ERC20BridgeSource.Native, + makerAmount: orderbookOrder1.makerAssetAmount, + takerAmount: orderbookOrder1.takerAssetAmount, + orderHash: orderHashUtils.getOrderHash(orderbookOrder1), + nativeOrder: orderbookOrder1, + fillableTakerAmount: orderbookOrder1FillableAmount, + isRfqt: false, + }; + const kyber1Source: BridgeReportSource = { + liquiditySource: ERC20BridgeSource.Kyber, + makerAmount: kyberSample1.output, + takerAmount: kyberSample1.input, + }; + const twoHopSource: MultiHopReportSource = { + liquiditySource: ERC20BridgeSource.MultiHop, + makerAmount: twoHopSample.output, + takerAmount: twoHopSample.input, + hopSources: [ERC20BridgeSource.Balancer, ERC20BridgeSource.Curve], + }; + + const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource]; + 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}`, + ); + }); + + expect(orderReport.sourcesDelivered.length).to.eql(1); + expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource); + }); }); diff --git a/packages/asset-swapper/test/utils/mock_sampler_contract.ts b/packages/asset-swapper/test/utils/mock_sampler_contract.ts index 15f928406b..6c51833821 100644 --- a/packages/asset-swapper/test/utils/mock_sampler_contract.ts +++ b/packages/asset-swapper/test/utils/mock_sampler_contract.ts @@ -29,7 +29,7 @@ export type SampleSellsLPHandler = ( takerToken: string, makerToken: string, takerTokenAmounts: BigNumber[], -) => SampleResults; +) => [SampleResults, string]; export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults; export type SampleSellsMBHandler = ( multiBridgeAddress: string, @@ -40,7 +40,7 @@ export type SampleSellsMBHandler = ( ) => SampleResults; const DUMMY_PROVIDER = { - sendAsync: (...args: any[]): any => { + sendAsync: (..._args: any[]): any => { /* no-op */ }, }; @@ -73,7 +73,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { public batchCall(callDatas: string[]): ContractFunctionObj { return { ...super.batchCall(callDatas), - callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)), + callAsync: async (..._callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)), }; } @@ -107,7 +107,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleSellsFromKyberNetwork, this._handlers.sampleSellsFromKyberNetwork, @@ -121,7 +121,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleSellsFromEth2Dai, this._handlers.sampleSellsFromEth2Dai, @@ -135,7 +135,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleSellsFromUniswap, this._handlers.sampleSellsFromUniswap, @@ -145,10 +145,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { ); } - public sampleSellsFromUniswapV2( - path: string[], - takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + public sampleSellsFromUniswapV2(path: string[], takerAssetAmounts: BigNumber[]): ContractFunctionObj { return this._wrapCall( super.sampleSellsFromUniswapV2, this._handlers.sampleSellsFromUniswapV2, @@ -162,7 +159,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj<[BigNumber[], string]> { return this._wrapCall( super.sampleSellsFromLiquidityProviderRegistry, this._handlers.sampleSellsFromLiquidityProviderRegistry, @@ -179,7 +176,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { intermediateToken: string, makerToken: string, takerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleSellsFromMultiBridge, this._handlers.sampleSellsFromMultiBridge, @@ -195,7 +192,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, makerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleBuysFromEth2Dai, this._handlers.sampleBuysFromEth2Dai, @@ -209,7 +206,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { takerToken: string, makerToken: string, makerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + ): ContractFunctionObj { return this._wrapCall( super.sampleBuysFromUniswap, this._handlers.sampleBuysFromUniswap, @@ -219,10 +216,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { ); } - public sampleBuysFromUniswapV2( - path: string[], - makerAssetAmounts: BigNumber[], - ): ContractFunctionObj { + public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractFunctionObj { return this._wrapCall( super.sampleBuysFromUniswapV2, this._handlers.sampleBuysFromUniswapV2, @@ -241,7 +235,12 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { if (handler && this.getSelector(name) === selector) { const args = this.getABIDecodedTransactionData(name, callData); const result = (handler as any)(...args); - return this._lookupAbiEncoder(this.getFunctionSignature(name)).encodeReturnValues([result]); + const encoder = this._lookupAbiEncoder(this.getFunctionSignature(name)); + if (encoder.getReturnValueDataItem().components!.length === 1) { + return encoder.encodeReturnValues([result]); + } else { + return encoder.encodeReturnValues(result); + } } } if (selector === this.getSelector('batchCall')) { @@ -260,7 +259,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract { ): ContractFunctionObj { return { ...superFn.call(this, ...args), - callAsync: async (...callArgs: any[]): Promise => { + callAsync: async (..._callArgs: any[]): Promise => { if (!handler) { throw new Error(`${superFn.name} handler undefined`); } diff --git a/packages/asset-swapper/test/utils/swap_quote.ts b/packages/asset-swapper/test/utils/swap_quote.ts index 2f8838e3aa..0dc6d72c6d 100644 --- a/packages/asset-swapper/test/utils/swap_quote.ts +++ b/packages/asset-swapper/test/utils/swap_quote.ts @@ -39,6 +39,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( bestCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo, sourceBreakdown: breakdown, + isTwoHop: false, }; if (operation === MarketOperation.Buy) { diff --git a/packages/asset-swapper/test/wrappers.ts b/packages/asset-swapper/test/wrappers.ts index a17bb121d7..30ea705ee3 100644 --- a/packages/asset-swapper/test/wrappers.ts +++ b/packages/asset-swapper/test/wrappers.ts @@ -4,11 +4,13 @@ * ----------------------------------------------------------------------------- */ export * from '../test/generated-wrappers/approximate_buys'; +export * from '../test/generated-wrappers/balancer_sampler'; export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/dummy_liquidity_provider'; export * from '../test/generated-wrappers/dummy_liquidity_provider_registry'; export * from '../test/generated-wrappers/erc20_bridge_sampler'; export * from '../test/generated-wrappers/eth2_dai_sampler'; +export * from '../test/generated-wrappers/i_balancer'; export * from '../test/generated-wrappers/i_curve'; export * from '../test/generated-wrappers/i_eth2_dai'; export * from '../test/generated-wrappers/i_kyber_hint_handler'; @@ -31,5 +33,6 @@ export * from '../test/generated-wrappers/native_order_sampler'; export * from '../test/generated-wrappers/sampler_utils'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; export * from '../test/generated-wrappers/test_native_order_sampler'; +export * from '../test/generated-wrappers/two_hop_sampler'; export * from '../test/generated-wrappers/uniswap_sampler'; export * from '../test/generated-wrappers/uniswap_v2_sampler'; diff --git a/packages/asset-swapper/tsconfig.json b/packages/asset-swapper/tsconfig.json index 852a297f6d..768accac03 100644 --- a/packages/asset-swapper/tsconfig.json +++ b/packages/asset-swapper/tsconfig.json @@ -9,11 +9,13 @@ "generated-artifacts/ILiquidityProvider.json", "generated-artifacts/ILiquidityProviderRegistry.json", "test/generated-artifacts/ApproximateBuys.json", + "test/generated-artifacts/BalancerSampler.json", "test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/DummyLiquidityProvider.json", "test/generated-artifacts/DummyLiquidityProviderRegistry.json", "test/generated-artifacts/ERC20BridgeSampler.json", "test/generated-artifacts/Eth2DaiSampler.json", + "test/generated-artifacts/IBalancer.json", "test/generated-artifacts/ICurve.json", "test/generated-artifacts/IEth2Dai.json", "test/generated-artifacts/IKyberHintHandler.json", @@ -36,6 +38,7 @@ "test/generated-artifacts/SamplerUtils.json", "test/generated-artifacts/TestERC20BridgeSampler.json", "test/generated-artifacts/TestNativeOrderSampler.json", + "test/generated-artifacts/TwoHopSampler.json", "test/generated-artifacts/UniswapSampler.json", "test/generated-artifacts/UniswapV2Sampler.json" ] diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index 2dc7f74a35..1afeec54a6 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Update `ERC20BridgeSampler` artifact", "pr": 2633 + }, + { + "note": "Remove `ERC20BridgeSampler` artifact", + "pr": 2647 } ] }, diff --git a/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json b/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json deleted file mode 100644 index f4a0445322..0000000000 --- a/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json +++ /dev/null @@ -1,475 +0,0 @@ -{ - "schemaVersion": "2.0.0", - "contractName": "IERC20BridgeSampler", - "compilerOutput": { - "abi": [ - { - "constant": true, - "inputs": [{ "internalType": "bytes[]", "name": "callDatas", "type": "bytes[]" }], - "name": "batchCall", - "outputs": [{ "internalType": "bytes[]", "name": "callResults", "type": "bytes[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "registryAddress", "type": "address" }, - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" } - ], - "name": "getLiquidityProviderFromRegistry", - "outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "components": [ - { "internalType": "address", "name": "makerAddress", "type": "address" }, - { "internalType": "address", "name": "takerAddress", "type": "address" }, - { "internalType": "address", "name": "feeRecipientAddress", "type": "address" }, - { "internalType": "address", "name": "senderAddress", "type": "address" }, - { "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "makerFee", "type": "uint256" }, - { "internalType": "uint256", "name": "takerFee", "type": "uint256" }, - { "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" }, - { "internalType": "uint256", "name": "salt", "type": "uint256" }, - { "internalType": "bytes", "name": "makerAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "takerAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" } - ], - "internalType": "struct LibOrder.Order[]", - "name": "orders", - "type": "tuple[]" - }, - { "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" }, - { "internalType": "address", "name": "devUtilsAddress", "type": "address" } - ], - "name": "getOrderFillableMakerAssetAmounts", - "outputs": [ - { "internalType": "uint256[]", "name": "orderFillableMakerAssetAmounts", "type": "uint256[]" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "components": [ - { "internalType": "address", "name": "makerAddress", "type": "address" }, - { "internalType": "address", "name": "takerAddress", "type": "address" }, - { "internalType": "address", "name": "feeRecipientAddress", "type": "address" }, - { "internalType": "address", "name": "senderAddress", "type": "address" }, - { "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "makerFee", "type": "uint256" }, - { "internalType": "uint256", "name": "takerFee", "type": "uint256" }, - { "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" }, - { "internalType": "uint256", "name": "salt", "type": "uint256" }, - { "internalType": "bytes", "name": "makerAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "takerAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" }, - { "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" } - ], - "internalType": "struct LibOrder.Order[]", - "name": "orders", - "type": "tuple[]" - }, - { "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" }, - { "internalType": "address", "name": "devUtilsAddress", "type": "address" } - ], - "name": "getOrderFillableTakerAssetAmounts", - "outputs": [ - { "internalType": "uint256[]", "name": "orderFillableTakerAssetAmounts", "type": "uint256[]" } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "curveAddress", "type": "address" }, - { "internalType": "int128", "name": "fromTokenIdx", "type": "int128" }, - { "internalType": "int128", "name": "toTokenIdx", "type": "int128" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleBuysFromCurve", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleBuysFromEth2Dai", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }, - { - "components": [ - { "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" }, - { "internalType": "uint256", "name": "maxIterations", "type": "uint256" } - ], - "internalType": "struct IERC20BridgeSampler.FakeBuyOptions", - "name": "opts", - "type": "tuple" - } - ], - "name": "sampleBuysFromKyberNetwork", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "registryAddress", "type": "address" }, - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }, - { - "components": [ - { "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" }, - { "internalType": "uint256", "name": "maxIterations", "type": "uint256" } - ], - "internalType": "struct IERC20BridgeSampler.FakeBuyOptions", - "name": "opts", - "type": "tuple" - } - ], - "name": "sampleBuysFromLiquidityProviderRegistry", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleBuysFromUniswap", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address[]", "name": "path", "type": "address[]" }, - { "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleBuysFromUniswapV2", - "outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "curveAddress", "type": "address" }, - { "internalType": "int128", "name": "fromTokenIdx", "type": "int128" }, - { "internalType": "int128", "name": "toTokenIdx", "type": "int128" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromCurve", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromEth2Dai", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromKyberNetwork", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "registryAddress", "type": "address" }, - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromLiquidityProviderRegistry", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "multibridge", "type": "address" }, - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "intermediateToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromMultiBridge", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address", "name": "takerToken", "type": "address" }, - { "internalType": "address", "name": "makerToken", "type": "address" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromUniswap", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "internalType": "address[]", "name": "path", "type": "address[]" }, - { "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" } - ], - "name": "sampleSellsFromUniswapV2", - "outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }], - "payable": false, - "stateMutability": "view", - "type": "function" - } - ], - "devdoc": { - "methods": { - "batchCall(bytes[])": { - "details": "Call multiple public functions on this contract in a single transaction.", - "params": { "callDatas": "ABI-encoded call data for each function call." }, - "return": "callResults ABI-encoded results data for each call." - }, - "getLiquidityProviderFromRegistry(address,address,address)": { - "details": "Returns the address of a liquidity provider for the given market (takerToken, makerToken), from a registry of liquidity providers. Returns address(0) if no such provider exists in the registry.", - "params": { - "makerToken": "Maker asset managed by liquidity provider.", - "takerToken": "Taker asset managed by liquidity provider." - }, - "return": "providerAddress Address of the liquidity provider." - }, - "getOrderFillableMakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": { - "details": "Queries the fillable maker asset amounts of native orders.", - "params": { - "devUtilsAddress": "Address to the DevUtils contract.", - "orderSignatures": "Signatures for each respective order in `orders`.", - "orders": "Native orders to query." - }, - "return": "orderFillableMakerAssetAmounts How much maker asset can be filled by each order in `orders`." - }, - "getOrderFillableTakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": { - "details": "Queries the fillable taker asset amounts of native orders.", - "params": { - "devUtilsAddress": "Address to the DevUtils contract.", - "orderSignatures": "Signatures for each respective order in `orders`.", - "orders": "Native orders to query." - }, - "return": "orderFillableTakerAssetAmounts How much taker asset can be filled by each order in `orders`." - }, - "sampleBuysFromCurve(address,int128,int128,uint256[])": { - "details": "Sample buy quotes from Curve.", - "params": { - "curveAddress": "Address of the Curve contract.", - "fromTokenIdx": "Index of the taker token (what to sell).", - "makerTokenAmounts": "Maker token buy amount for each sample.", - "toTokenIdx": "Index of the maker token (what to buy)." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleBuysFromEth2Dai(address,address,uint256[])": { - "details": "Sample buy quotes from Eth2Dai/Oasis.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "makerTokenAmounts": "Maker token buy amount for each sample.", - "takerToken": "Address of the taker token (what to sell)." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleBuysFromKyberNetwork(address,address,uint256[],(uint256,uint256))": { - "details": "Sample buy quotes from Kyber.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "makerTokenAmounts": "Maker token buy amount for each sample.", - "opts": "`FakeBuyOptions` specifying target slippage and max iterations.", - "takerToken": "Address of the taker token (what to sell)." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleBuysFromLiquidityProviderRegistry(address,address,address,uint256[],(uint256,uint256))": { - "details": "Sample buy quotes from an arbitrary on-chain liquidity provider.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "makerTokenAmounts": "Maker token buy amount for each sample.", - "opts": "`FakeBuyOptions` specifying target slippage and max iterations.", - "registryAddress": "Address of the liquidity provider registry contract.", - "takerToken": "Address of the taker token (what to sell)." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleBuysFromUniswap(address,address,uint256[])": { - "details": "Sample buy quotes from Uniswap.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "makerTokenAmounts": "Maker token buy amount for each sample.", - "takerToken": "Address of the taker token (what to sell)." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleBuysFromUniswapV2(address[],uint256[])": { - "details": "Sample buy quotes from UniswapV2.", - "params": { - "makerTokenAmounts": "Maker token buy amount for each sample.", - "path": "Token route." - }, - "return": "takerTokenAmounts Taker amounts sold at each maker token amount." - }, - "sampleSellsFromCurve(address,int128,int128,uint256[])": { - "details": "Sample sell quotes from Curve.", - "params": { - "curveAddress": "Address of the Curve contract.", - "fromTokenIdx": "Index of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample.", - "toTokenIdx": "Index of the maker token (what to buy)." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromEth2Dai(address,address,uint256[])": { - "details": "Sample sell quotes from Eth2Dai/Oasis.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "takerToken": "Address of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromKyberNetwork(address,address,uint256[])": { - "details": "Sample sell quotes from Kyber.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "takerToken": "Address of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromLiquidityProviderRegistry(address,address,address,uint256[])": { - "details": "Sample sell quotes from an arbitrary on-chain liquidity provider.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "registryAddress": "Address of the liquidity provider registry contract.", - "takerToken": "Address of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromMultiBridge(address,address,address,address,uint256[])": { - "details": "Sample sell quotes from MultiBridge.", - "params": { - "intermediateToken": "The address of the intermediate token to use in an indirect route.", - "makerToken": "Address of the maker token (what to buy).", - "multibridge": "Address of the MultiBridge contract.", - "takerToken": "Address of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromUniswap(address,address,uint256[])": { - "details": "Sample sell quotes from Uniswap.", - "params": { - "makerToken": "Address of the maker token (what to buy).", - "takerToken": "Address of the taker token (what to sell).", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - }, - "sampleSellsFromUniswapV2(address[],uint256[])": { - "details": "Sample sell quotes from UniswapV2.", - "params": { - "path": "Token route.", - "takerTokenAmounts": "Taker token sell amount for each sample." - }, - "return": "makerTokenAmounts Maker amounts bought at each taker token amount." - } - } - }, - "evm": { "bytecode": { "object": "0x" }, "deployedBytecode": { "object": "0x" } } - }, - "compiler": { - "name": "solc", - "version": "0.5.17+commit.d19bba13", - "settings": { - "optimizer": { - "enabled": true, - "runs": 1000000, - "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "devdoc", - "evm.bytecode.object", - "evm.bytecode.sourceMap", - "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" - ] - } - }, - "evmVersion": "istanbul" - } - }, - "chains": {} -} diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index 9e209e2964..214683f6a4 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -32,7 +32,7 @@ "wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers" }, "config": { - "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json" + "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json" }, "gitpkg": { "registry": "git@github.com:0xProject/gitpkg-registry.git" diff --git a/tsconfig.json b/tsconfig.json index e68f896893..7f114cd28d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es6", "lib": ["es2017", "dom", "esnext.asynciterable", "es2018.promise"], "experimentalDecorators": true, "downlevelIteration": true,