BalancerBridge (#2613)
* Add BalancerBridge and Sampler functions * Update sampler artifacts/wrappers * Add Balancer support to AssetSwapper + related refactoring * Make use of GraphQL instead of sampler * "fix" build and add mainnet BalancerBridge tests * address some comments * add balancer cache and fix DexSampler tests * lint * wip: tests for balancer sampler ops * Fix market operation utils test * balancer unit tests * Return a buy quote of 0 if the buy amount exceeds the Balancer pool's balance * Dynamic fee estimation * Update contract addresses, export BalancerBridge wrapper * Update changelogs * Fix bugs discovered via simbot * Fix issues in balancer_utils * override `BigNumber.config` in configured_bignumber.ts * Special case Balancer subops in too * Address some more comments * Address Balancer performance issue * Performance improvements * Address comment * Fix tests Co-authored-by: xianny <xianny@gmail.com>
This commit is contained in:
		| @@ -1,10 +1,14 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "3.3.1", | ||||
|         "version": "3.4.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Fix instability with DFB.", | ||||
|                 "pr": 2616 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Add `BalancerBridge`", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										103
									
								
								contracts/asset-proxy/contracts/src/bridges/BalancerBridge.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								contracts/asset-proxy/contracts/src/bridges/BalancerBridge.sol
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
|  | ||||
| /* | ||||
|  | ||||
|   Copyright 2020 ZeroEx Intl. | ||||
|  | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
|  | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.5.9; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; | ||||
| import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; | ||||
| import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; | ||||
| import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; | ||||
| import "../interfaces/IERC20Bridge.sol"; | ||||
| import "../interfaces/IBalancerPool.sol"; | ||||
|  | ||||
|  | ||||
| contract BalancerBridge is | ||||
|     IERC20Bridge, | ||||
|     IWallet, | ||||
|     DeploymentConstants | ||||
| { | ||||
|     /// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of | ||||
|     ///      `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress` | ||||
|     ///      token encoded in the bridge data, then transfers the bought | ||||
|     ///      tokens to `to`. | ||||
|     /// @param toTokenAddress The token to buy and transfer to `to`. | ||||
|     /// @param from The maker (this contract). | ||||
|     /// @param to The recipient of the bought tokens. | ||||
|     /// @param amount Minimum amount of `toTokenAddress` tokens to buy. | ||||
|     /// @param bridgeData The abi-encoded addresses of the "from" token and Balancer pool. | ||||
|     /// @return success The magic bytes if successful. | ||||
|     function bridgeTransferFrom( | ||||
|         address toTokenAddress, | ||||
|         address from, | ||||
|         address to, | ||||
|         uint256 amount, | ||||
|         bytes calldata bridgeData | ||||
|     ) | ||||
|         external | ||||
|         returns (bytes4 success) | ||||
|     { | ||||
|         // Decode the bridge data. | ||||
|         (address fromTokenAddress, address poolAddress) = abi.decode( | ||||
|             bridgeData, | ||||
|             (address, address) | ||||
|         ); | ||||
|         require(toTokenAddress != fromTokenAddress, "BalancerBridge/INVALID_PAIR"); | ||||
|  | ||||
|         uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); | ||||
|         // Grant an allowance to the exchange to spend `fromTokenAddress` token. | ||||
|         LibERC20Token.approveIfBelow(fromTokenAddress, poolAddress, fromTokenBalance); | ||||
|  | ||||
|         // Sell all of this contract's `fromTokenAddress` token balance. | ||||
|         (uint256 boughtAmount,) = IBalancerPool(poolAddress).swapExactAmountIn( | ||||
|             fromTokenAddress, // tokenIn | ||||
|             fromTokenBalance, // tokenAmountIn | ||||
|             toTokenAddress,   // tokenOut | ||||
|             amount,           // minAmountOut | ||||
|             uint256(-1)       // maxPrice | ||||
|         ); | ||||
|  | ||||
|         // Transfer the converted `toToken`s to `to`. | ||||
|         LibERC20Token.transfer(toTokenAddress, to, boughtAmount); | ||||
|  | ||||
|         emit ERC20BridgeTransfer( | ||||
|             fromTokenAddress, | ||||
|             toTokenAddress, | ||||
|             fromTokenBalance, | ||||
|             boughtAmount, | ||||
|             from, | ||||
|             to | ||||
|         ); | ||||
|         return BRIDGE_SUCCESS; | ||||
|     } | ||||
|  | ||||
|     /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker | ||||
|     ///      and sign for itself in orders. Always succeeds. | ||||
|     /// @return magicValue Magic success bytes, always. | ||||
|     function isValidSignature( | ||||
|         bytes32, | ||||
|         bytes calldata | ||||
|     ) | ||||
|         external | ||||
|         view | ||||
|         returns (bytes4 magicValue) | ||||
|     { | ||||
|         return LEGACY_WALLET_MAGIC_VALUE; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| /* | ||||
|  | ||||
|   Copyright 2020 ZeroEx Intl. | ||||
|  | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
|  | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.5.9; | ||||
|  | ||||
|  | ||||
| interface IBalancerPool { | ||||
|     /// @dev Sell `tokenAmountIn` of `tokenIn` and receive `tokenOut`. | ||||
|     /// @param tokenIn The token being sold | ||||
|     /// @param tokenAmountIn The amount of `tokenIn` to sell. | ||||
|     /// @param tokenOut The token being bought. | ||||
|     /// @param minAmountOut The minimum amount of `tokenOut` to buy. | ||||
|     /// @param maxPrice The maximum value for `spotPriceAfter`. | ||||
|     /// @return tokenAmountOut The amount of `tokenOut` bought. | ||||
|     /// @return spotPriceAfter The new marginal spot price of the given | ||||
|     ///         token pair for this pool. | ||||
|     function swapExactAmountIn( | ||||
|         address tokenIn, | ||||
|         uint tokenAmountIn, | ||||
|         address tokenOut, | ||||
|         uint minAmountOut, | ||||
|         uint maxPrice | ||||
|     ) external returns (uint tokenAmountOut, uint spotPriceAfter); | ||||
| } | ||||
| @@ -38,7 +38,7 @@ | ||||
|         "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" | ||||
|     }, | ||||
|     "config": { | ||||
|         "abis": "./test/generated-artifacts/@(ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", | ||||
|         "abis": "./test/generated-artifacts/@(BalancerBridge|ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", | ||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." | ||||
|     }, | ||||
|     "repository": { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  */ | ||||
| import { ContractArtifact } from 'ethereum-types'; | ||||
|  | ||||
| import * as BalancerBridge from '../generated-artifacts/BalancerBridge.json'; | ||||
| import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json'; | ||||
| import * as CurveBridge from '../generated-artifacts/CurveBridge.json'; | ||||
| import * as DexForwarderBridge from '../generated-artifacts/DexForwarderBridge.json'; | ||||
| @@ -18,6 +19,7 @@ import * as IAssetData from '../generated-artifacts/IAssetData.json'; | ||||
| import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; | ||||
| import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json'; | ||||
| import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; | ||||
| import * as IBalancerPool from '../generated-artifacts/IBalancerPool.json'; | ||||
| import * as IChai from '../generated-artifacts/IChai.json'; | ||||
| import * as ICurve from '../generated-artifacts/ICurve.json'; | ||||
| import * as IDydx from '../generated-artifacts/IDydx.json'; | ||||
| @@ -57,6 +59,7 @@ export const artifacts = { | ||||
|     ERC721Proxy: ERC721Proxy as ContractArtifact, | ||||
|     MultiAssetProxy: MultiAssetProxy as ContractArtifact, | ||||
|     StaticCallProxy: StaticCallProxy as ContractArtifact, | ||||
|     BalancerBridge: BalancerBridge as ContractArtifact, | ||||
|     ChaiBridge: ChaiBridge as ContractArtifact, | ||||
|     CurveBridge: CurveBridge as ContractArtifact, | ||||
|     DexForwarderBridge: DexForwarderBridge as ContractArtifact, | ||||
| @@ -70,6 +73,7 @@ export const artifacts = { | ||||
|     IAssetProxy: IAssetProxy as ContractArtifact, | ||||
|     IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, | ||||
|     IAuthorizable: IAuthorizable as ContractArtifact, | ||||
|     IBalancerPool: IBalancerPool as ContractArtifact, | ||||
|     IChai: IChai as ContractArtifact, | ||||
|     ICurve: ICurve as ContractArtifact, | ||||
|     IDydx: IDydx as ContractArtifact, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| export { artifacts } from './artifacts'; | ||||
| export { | ||||
|     BalancerBridgeContract, | ||||
|     ChaiBridgeContract, | ||||
|     ERC1155ProxyContract, | ||||
|     ERC20BridgeProxyContract, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|  * Warning: This file is auto-generated by contracts-gen. Don't edit manually. | ||||
|  * ----------------------------------------------------------------------------- | ||||
|  */ | ||||
| export * from '../generated-wrappers/balancer_bridge'; | ||||
| export * from '../generated-wrappers/chai_bridge'; | ||||
| export * from '../generated-wrappers/curve_bridge'; | ||||
| export * from '../generated-wrappers/dex_forwarder_bridge'; | ||||
| @@ -16,6 +17,7 @@ export * from '../generated-wrappers/i_asset_data'; | ||||
| export * from '../generated-wrappers/i_asset_proxy'; | ||||
| export * from '../generated-wrappers/i_asset_proxy_dispatcher'; | ||||
| export * from '../generated-wrappers/i_authorizable'; | ||||
| export * from '../generated-wrappers/i_balancer_pool'; | ||||
| export * from '../generated-wrappers/i_chai'; | ||||
| export * from '../generated-wrappers/i_curve'; | ||||
| export * from '../generated-wrappers/i_dydx'; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  */ | ||||
| import { ContractArtifact } from 'ethereum-types'; | ||||
|  | ||||
| import * as BalancerBridge from '../test/generated-artifacts/BalancerBridge.json'; | ||||
| import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json'; | ||||
| import * as CurveBridge from '../test/generated-artifacts/CurveBridge.json'; | ||||
| import * as DexForwarderBridge from '../test/generated-artifacts/DexForwarderBridge.json'; | ||||
| @@ -18,6 +19,7 @@ import * as IAssetData from '../test/generated-artifacts/IAssetData.json'; | ||||
| import * as IAssetProxy from '../test/generated-artifacts/IAssetProxy.json'; | ||||
| import * as IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyDispatcher.json'; | ||||
| import * as IAuthorizable from '../test/generated-artifacts/IAuthorizable.json'; | ||||
| import * as IBalancerPool from '../test/generated-artifacts/IBalancerPool.json'; | ||||
| import * as IChai from '../test/generated-artifacts/IChai.json'; | ||||
| import * as ICurve from '../test/generated-artifacts/ICurve.json'; | ||||
| import * as IDydx from '../test/generated-artifacts/IDydx.json'; | ||||
| @@ -57,6 +59,7 @@ export const artifacts = { | ||||
|     ERC721Proxy: ERC721Proxy as ContractArtifact, | ||||
|     MultiAssetProxy: MultiAssetProxy as ContractArtifact, | ||||
|     StaticCallProxy: StaticCallProxy as ContractArtifact, | ||||
|     BalancerBridge: BalancerBridge as ContractArtifact, | ||||
|     ChaiBridge: ChaiBridge as ContractArtifact, | ||||
|     CurveBridge: CurveBridge as ContractArtifact, | ||||
|     DexForwarderBridge: DexForwarderBridge as ContractArtifact, | ||||
| @@ -70,6 +73,7 @@ export const artifacts = { | ||||
|     IAssetProxy: IAssetProxy as ContractArtifact, | ||||
|     IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, | ||||
|     IAuthorizable: IAuthorizable as ContractArtifact, | ||||
|     IBalancerPool: IBalancerPool as ContractArtifact, | ||||
|     IChai: IChai as ContractArtifact, | ||||
|     ICurve: ICurve as ContractArtifact, | ||||
|     IDydx: IDydx as ContractArtifact, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|  * Warning: This file is auto-generated by contracts-gen. Don't edit manually. | ||||
|  * ----------------------------------------------------------------------------- | ||||
|  */ | ||||
| export * from '../test/generated-wrappers/balancer_bridge'; | ||||
| export * from '../test/generated-wrappers/chai_bridge'; | ||||
| export * from '../test/generated-wrappers/curve_bridge'; | ||||
| export * from '../test/generated-wrappers/dex_forwarder_bridge'; | ||||
| @@ -16,6 +17,7 @@ export * from '../test/generated-wrappers/i_asset_data'; | ||||
| export * from '../test/generated-wrappers/i_asset_proxy'; | ||||
| export * from '../test/generated-wrappers/i_asset_proxy_dispatcher'; | ||||
| export * from '../test/generated-wrappers/i_authorizable'; | ||||
| export * from '../test/generated-wrappers/i_balancer_pool'; | ||||
| export * from '../test/generated-wrappers/i_chai'; | ||||
| export * from '../test/generated-wrappers/i_curve'; | ||||
| export * from '../test/generated-wrappers/i_dydx'; | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|     "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, | ||||
|     "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], | ||||
|     "files": [ | ||||
|         "generated-artifacts/BalancerBridge.json", | ||||
|         "generated-artifacts/ChaiBridge.json", | ||||
|         "generated-artifacts/CurveBridge.json", | ||||
|         "generated-artifacts/DexForwarderBridge.json", | ||||
| @@ -16,6 +17,7 @@ | ||||
|         "generated-artifacts/IAssetProxy.json", | ||||
|         "generated-artifacts/IAssetProxyDispatcher.json", | ||||
|         "generated-artifacts/IAuthorizable.json", | ||||
|         "generated-artifacts/IBalancerPool.json", | ||||
|         "generated-artifacts/IChai.json", | ||||
|         "generated-artifacts/ICurve.json", | ||||
|         "generated-artifacts/IDydx.json", | ||||
| @@ -45,6 +47,7 @@ | ||||
|         "generated-artifacts/TestUniswapV2Bridge.json", | ||||
|         "generated-artifacts/UniswapBridge.json", | ||||
|         "generated-artifacts/UniswapV2Bridge.json", | ||||
|         "test/generated-artifacts/BalancerBridge.json", | ||||
|         "test/generated-artifacts/ChaiBridge.json", | ||||
|         "test/generated-artifacts/CurveBridge.json", | ||||
|         "test/generated-artifacts/DexForwarderBridge.json", | ||||
| @@ -58,6 +61,7 @@ | ||||
|         "test/generated-artifacts/IAssetProxy.json", | ||||
|         "test/generated-artifacts/IAssetProxyDispatcher.json", | ||||
|         "test/generated-artifacts/IAuthorizable.json", | ||||
|         "test/generated-artifacts/IBalancerPool.json", | ||||
|         "test/generated-artifacts/IChai.json", | ||||
|         "test/generated-artifacts/ICurve.json", | ||||
|         "test/generated-artifacts/IDydx.json", | ||||
|   | ||||
| @@ -1,4 +1,13 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "2.6.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Add `BalancerBridge` mainnet tests", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "version": "2.5.2", | ||||
|         "changes": [ | ||||
|   | ||||
| @@ -0,0 +1,103 @@ | ||||
| import { artifacts as assetProxyArtifacts } from '@0x/contracts-asset-proxy'; | ||||
| import { BalancerBridgeContract } from '@0x/contracts-asset-proxy/lib/src/wrappers'; | ||||
| import { ERC20TokenContract } from '@0x/contracts-erc20'; | ||||
| import { blockchainTests, constants, expect, toBaseUnitAmount } from '@0x/contracts-test-utils'; | ||||
| import { AbiEncoder } from '@0x/utils'; | ||||
|  | ||||
| const CHONKY_DAI_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e'; // dydx solo margin | ||||
| const CHONKY_WETH_WALLET = '0x2f0b23f53734252bda2277357e97e1517d6b042a'; // MCD wETH vault | ||||
| const CHONKY_USDC_WALLET = '0x39aa39c021dfbae8fac545936693ac917d5e7563'; // Compound | ||||
| blockchainTests.configure({ | ||||
|     fork: { | ||||
|         unlockedAccounts: [CHONKY_USDC_WALLET, CHONKY_WETH_WALLET, CHONKY_DAI_WALLET], | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| blockchainTests.fork('Mainnet Balancer bridge tests', env => { | ||||
|     let testContract: BalancerBridgeContract; | ||||
|     let weth: ERC20TokenContract; | ||||
|     let usdc: ERC20TokenContract; | ||||
|     const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308'; | ||||
|     const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; | ||||
|     const wethAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; | ||||
|     const wethUsdcBalancerAddress = '0x2471de1547296aadb02cc1af84afe369b6f67c87'; | ||||
|     const wethUsdcDaiBalancerAddress = '0x9b208194acc0a8ccb2a8dcafeacfbb7dcc093f81'; | ||||
|     const bridgeDataEncoder = AbiEncoder.create([ | ||||
|         { name: 'takerToken', type: 'address' }, | ||||
|         { name: 'poolAddress', type: 'address' }, | ||||
|     ]); | ||||
|  | ||||
|     before(async () => { | ||||
|         testContract = await BalancerBridgeContract.deployFrom0xArtifactAsync( | ||||
|             assetProxyArtifacts.BalancerBridge, | ||||
|             env.provider, | ||||
|             { ...env.txDefaults, from: CHONKY_DAI_WALLET, gasPrice: 0 }, | ||||
|             {}, | ||||
|         ); | ||||
|         weth = new ERC20TokenContract(wethAddress, env.provider, env.txDefaults); | ||||
|         usdc = new ERC20TokenContract(usdcAddress, env.provider, env.txDefaults); | ||||
|     }); | ||||
|  | ||||
|     blockchainTests.resets('Can trade with two-asset pool', () => { | ||||
|         it('successfully exchanges WETH for USDC', async () => { | ||||
|             const bridgeData = bridgeDataEncoder.encode([wethAddress, wethUsdcBalancerAddress]); | ||||
|             // Fund the Bridge | ||||
|             await weth | ||||
|                 .transfer(testContract.address, toBaseUnitAmount(1)) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const usdcBalanceBefore = await usdc.balanceOf(receiver).callAsync(); | ||||
|             // Exchange via Balancer | ||||
|             await testContract | ||||
|                 .bridgeTransferFrom(usdcAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             // Check that USDC balance increased | ||||
|             const usdcBalanceAfter = await usdc.balanceOf(receiver).callAsync(); | ||||
|             expect(usdcBalanceAfter).to.be.bignumber.greaterThan(usdcBalanceBefore); | ||||
|         }); | ||||
|         it('successfully exchanges USDC for WETH', async () => { | ||||
|             const bridgeData = bridgeDataEncoder.encode([usdcAddress, wethUsdcBalancerAddress]); | ||||
|             // Fund the Bridge | ||||
|             await usdc | ||||
|                 .transfer(testContract.address, toBaseUnitAmount(1, 6)) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const wethBalanceBefore = await weth.balanceOf(receiver).callAsync(); | ||||
|             // Exchange via Balancer | ||||
|             await testContract | ||||
|                 .bridgeTransferFrom(wethAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const wethBalanceAfter = await weth.balanceOf(receiver).callAsync(); | ||||
|             expect(wethBalanceAfter).to.be.bignumber.greaterThan(wethBalanceBefore); | ||||
|         }); | ||||
|     }); | ||||
|     blockchainTests.resets('Can trade with three-asset pool', () => { | ||||
|         it('successfully exchanges WETH for USDC', async () => { | ||||
|             const bridgeData = bridgeDataEncoder.encode([wethAddress, wethUsdcDaiBalancerAddress]); | ||||
|             // Fund the Bridge | ||||
|             await weth | ||||
|                 .transfer(testContract.address, toBaseUnitAmount(1)) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const usdcBalanceBefore = await usdc.balanceOf(receiver).callAsync(); | ||||
|             // Exchange via Balancer | ||||
|             await testContract | ||||
|                 .bridgeTransferFrom(usdcAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_WETH_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             // Check that USDC balance increased | ||||
|             const usdcBalanceAfter = await usdc.balanceOf(receiver).callAsync(); | ||||
|             expect(usdcBalanceAfter).to.be.bignumber.greaterThan(usdcBalanceBefore); | ||||
|         }); | ||||
|         it('successfully exchanges USDC for WETH', async () => { | ||||
|             const bridgeData = bridgeDataEncoder.encode([usdcAddress, wethUsdcDaiBalancerAddress]); | ||||
|             // Fund the Bridge | ||||
|             await usdc | ||||
|                 .transfer(testContract.address, toBaseUnitAmount(1, 6)) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const wethBalanceBefore = await weth.balanceOf(receiver).callAsync(); | ||||
|             // Exchange via Balancer | ||||
|             await testContract | ||||
|                 .bridgeTransferFrom(wethAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) | ||||
|                 .awaitTransactionSuccessAsync({ from: CHONKY_USDC_WALLET, gasPrice: 0 }, { shouldValidate: false }); | ||||
|             const wethBalanceAfter = await weth.balanceOf(receiver).callAsync(); | ||||
|             expect(wethBalanceAfter).to.be.bignumber.greaterThan(wethBalanceBefore); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -25,6 +25,18 @@ | ||||
|             { | ||||
|                 "note": "\"Fix\" forwarder buys of low decimal tokens.", | ||||
|                 "pr": 2618 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Add Balancer support", | ||||
|                 "pr": 2613 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Consolidate UniswapV2 sources, Curve sources in `ERC20BridgeSource` enum", | ||||
|                 "pr": 2613 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Change gas/fee schedule values from constants to functions returning numbers", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -55,8 +55,10 @@ | ||||
|         "@0x/quote-server": "^2.0.2", | ||||
|         "@0x/utils": "^5.5.0", | ||||
|         "@0x/web3-wrapper": "^7.1.0", | ||||
|         "@balancer-labs/sor": "^0.3.0", | ||||
|         "axios": "^0.19.2", | ||||
|         "axios-mock-adapter": "^1.18.1", | ||||
|         "decimal.js": "^10.2.0", | ||||
|         "ethereumjs-util": "^5.1.1", | ||||
|         "heartbeats": "^5.0.1", | ||||
|         "lodash": "^4.17.11" | ||||
|   | ||||
| @@ -69,6 +69,12 @@ export { | ||||
|     NativeCollapsedFill, | ||||
|     OptimizedMarketOrder, | ||||
|     GetMarketOrdersRfqtOpts, | ||||
|     FeeSchedule, | ||||
|     FillData, | ||||
|     NativeFillData, | ||||
|     CurveFillData, | ||||
|     BalancerFillData, | ||||
|     UniswapV2FillData, | ||||
| } from './utils/market_operation_utils/types'; | ||||
| export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; | ||||
| export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; | ||||
|   | ||||
| @@ -0,0 +1,102 @@ | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; | ||||
| import { Decimal } from 'decimal.js'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| export interface BalancerPool { | ||||
|     id: string; | ||||
|     balanceIn: BigNumber; | ||||
|     balanceOut: BigNumber; | ||||
|     weightIn: BigNumber; | ||||
|     weightOut: BigNumber; | ||||
|     swapFee: BigNumber; | ||||
|     spotPrice?: BigNumber; | ||||
|     slippage?: BigNumber; | ||||
|     limitAmount?: BigNumber; | ||||
| } | ||||
|  | ||||
| interface CacheValue { | ||||
|     timestamp: number; | ||||
|     pools: BalancerPool[]; | ||||
| } | ||||
|  | ||||
| // tslint:disable:custom-no-magic-numbers | ||||
| const FIVE_SECONDS_MS = 5 * 1000; | ||||
| const DEFAULT_TIMEOUT_MS = 1000; | ||||
| 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, | ||||
|     ) {} | ||||
|  | ||||
|     public async getPoolsForPairAsync( | ||||
|         takerToken: string, | ||||
|         makerToken: string, | ||||
|         timeoutMs: number = DEFAULT_TIMEOUT_MS, | ||||
|     ): Promise<BalancerPool[]> { | ||||
|         const timeout = new Promise<BalancerPool[]>(resolve => setTimeout(resolve, timeoutMs, [])); | ||||
|         return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); | ||||
|     } | ||||
|  | ||||
|     protected async _getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> { | ||||
|         const key = JSON.stringify([takerToken, makerToken]); | ||||
|         const value = this._cache[key]; | ||||
|         const minTimestamp = Date.now() - this.cacheExpiryMs; | ||||
|         if (value === undefined || value.timestamp < minTimestamp) { | ||||
|             const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); | ||||
|             const timestamp = Date.now(); | ||||
|             this._cache[key] = { | ||||
|                 pools, | ||||
|                 timestamp, | ||||
|             }; | ||||
|         } | ||||
|         return this._cache[key].pools; | ||||
|     } | ||||
|  | ||||
|     // tslint:disable-next-line:prefer-function-over-method | ||||
|     protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> { | ||||
|         try { | ||||
|             const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; | ||||
|             // Sort by maker token balance (descending) | ||||
|             const pools = parsePoolData(poolData, takerToken, makerToken).sort((a, b) => | ||||
|                 b.balanceOut.minus(a.balanceOut).toNumber(), | ||||
|             ); | ||||
|             return pools.length > MAX_POOLS_FETCHED ? pools.slice(0, MAX_POOLS_FETCHED) : pools; | ||||
|         } catch (err) { | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // tslint:disable completed-docs | ||||
| export function computeBalancerSellQuote(pool: BalancerPool, takerFillAmount: BigNumber): BigNumber { | ||||
|     const weightRatio = pool.weightIn.dividedBy(pool.weightOut); | ||||
|     const adjustedIn = bmath.BONE.minus(pool.swapFee) | ||||
|         .dividedBy(bmath.BONE) | ||||
|         .times(takerFillAmount); | ||||
|     const y = pool.balanceIn.dividedBy(pool.balanceIn.plus(adjustedIn)); | ||||
|     const foo = Math.pow(y.toNumber(), weightRatio.toNumber()); | ||||
|     const bar = new BigNumber(1).minus(foo); | ||||
|     const tokenAmountOut = pool.balanceOut.times(bar); | ||||
|     return tokenAmountOut.integerValue(); | ||||
| } | ||||
|  | ||||
| export function computeBalancerBuyQuote(pool: BalancerPool, makerFillAmount: BigNumber): BigNumber { | ||||
|     if (makerFillAmount.isGreaterThanOrEqualTo(pool.balanceOut)) { | ||||
|         return new BigNumber(0); | ||||
|     } | ||||
|     const weightRatio = pool.weightOut.dividedBy(pool.weightIn); | ||||
|     const diff = pool.balanceOut.minus(makerFillAmount); | ||||
|     const y = pool.balanceOut.dividedBy(diff); | ||||
|     let foo: number | Decimal = Math.pow(y.toNumber(), weightRatio.toNumber()) - 1; | ||||
|     if (!Number.isFinite(foo)) { | ||||
|         foo = new Decimal20(y.toString()).pow(weightRatio.toString()).minus(1); | ||||
|     } | ||||
|     let tokenAmountIn = bmath.BONE.minus(pool.swapFee).dividedBy(bmath.BONE); | ||||
|     tokenAmountIn = pool.balanceIn.times(foo.toString()).dividedBy(tokenAmountIn); | ||||
|     return tokenAmountIn.integerValue(); | ||||
| } | ||||
| @@ -10,15 +10,10 @@ import { ERC20BridgeSource, FakeBuyOpts, GetMarketOrdersOpts } from './types'; | ||||
| export const SELL_SOURCES = [ | ||||
|     ERC20BridgeSource.Uniswap, | ||||
|     ERC20BridgeSource.UniswapV2, | ||||
|     ERC20BridgeSource.UniswapV2Eth, | ||||
|     ERC20BridgeSource.Eth2Dai, | ||||
|     ERC20BridgeSource.Kyber, | ||||
|     // All Curve Sources | ||||
|     ERC20BridgeSource.CurveUsdcDai, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdt, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtTusd, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtBusd, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtSusd, | ||||
|     ERC20BridgeSource.Curve, | ||||
|     ERC20BridgeSource.Balancer, | ||||
| ]; | ||||
|  | ||||
| /** | ||||
| @@ -27,15 +22,10 @@ export const SELL_SOURCES = [ | ||||
| export const BUY_SOURCES = [ | ||||
|     ERC20BridgeSource.Uniswap, | ||||
|     ERC20BridgeSource.UniswapV2, | ||||
|     ERC20BridgeSource.UniswapV2Eth, | ||||
|     ERC20BridgeSource.Eth2Dai, | ||||
|     ERC20BridgeSource.Kyber, | ||||
|     // All Curve sources | ||||
|     ERC20BridgeSource.CurveUsdcDai, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdt, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtBusd, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtTusd, | ||||
|     ERC20BridgeSource.CurveUsdcDaiUsdtSusd, | ||||
|     ERC20BridgeSource.Curve, | ||||
|     ERC20BridgeSource.Balancer, | ||||
| ]; | ||||
|  | ||||
| export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | ||||
| @@ -60,56 +50,44 @@ export const DEFAULT_FAKE_BUY_OPTS: FakeBuyOpts = { | ||||
| /** | ||||
|  * Sources to poll for ETH fee price estimates. | ||||
|  */ | ||||
| export const FEE_QUOTE_SOURCES = SELL_SOURCES; | ||||
| export const FEE_QUOTE_SOURCES = [ | ||||
|     ERC20BridgeSource.Uniswap, | ||||
|     ERC20BridgeSource.UniswapV2, | ||||
|     ERC20BridgeSource.Eth2Dai, | ||||
|     ERC20BridgeSource.Kyber, | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * Mainnet Curve configuration | ||||
|  */ | ||||
| export const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = { | ||||
|     [ERC20BridgeSource.CurveUsdcDai]: { | ||||
|         version: 1, | ||||
|         curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56', | ||||
|         tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'], | ||||
|     }, | ||||
|     [ERC20BridgeSource.CurveUsdcDaiUsdt]: { | ||||
|         version: 1, | ||||
|         curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c', | ||||
|         tokens: [ | ||||
|             '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|             '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|             '0xdac17f958d2ee523a2206206994597c13d831ec7', | ||||
|         ], | ||||
|     }, | ||||
|     [ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: { | ||||
|         version: 1, | ||||
|         curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51', | ||||
|         tokens: [ | ||||
|             '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|             '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|             '0xdac17f958d2ee523a2206206994597c13d831ec7', | ||||
|             '0x0000000000085d4780b73119b644ae5ecd22b376', | ||||
|         ], | ||||
|     }, | ||||
|     [ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: { | ||||
|         version: 1, | ||||
|         curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27', | ||||
|         tokens: [ | ||||
|             '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|             '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|             '0xdac17f958d2ee523a2206206994597c13d831ec7', | ||||
|             '0x4fabb145d64652a948d72533023f6e7a623c7c53', | ||||
|         ], | ||||
|     }, | ||||
|     [ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: { | ||||
|         version: 1, | ||||
|         curveAddress: '0xa5407eae9ba41422680e2e00537571bcc53efbfd', | ||||
|         tokens: [ | ||||
|             '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|             '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|             '0xdac17f958d2ee523a2206206994597c13d831ec7', | ||||
|             '0x57ab1ec28d129707052df4df418d58a2d46d5f51', | ||||
|         ], | ||||
|     }, | ||||
| export const MAINNET_CURVE_CONTRACTS: { [curveAddress: string]: string[] } = { | ||||
|     '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56': [ | ||||
|         '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI | ||||
|         '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | ||||
|     ], | ||||
|     '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c': [ | ||||
|         '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI | ||||
|         '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | ||||
|         '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT | ||||
|     ], | ||||
|     '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51': [ | ||||
|         '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI | ||||
|         '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | ||||
|         '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT | ||||
|         '0x0000000000085d4780b73119b644ae5ecd22b376', // TUSD | ||||
|     ], | ||||
|     '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27': [ | ||||
|         '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI | ||||
|         '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | ||||
|         '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT | ||||
|         '0x4fabb145d64652a948d72533023f6e7a623c7c53', // BUSD | ||||
|     ], | ||||
|     '0xa5407eae9ba41422680e2e00537571bcc53efbfd': [ | ||||
|         '0x6b175474e89094c44da98b954eedeac495271d0f', // DAI | ||||
|         '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC | ||||
|         '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT | ||||
|         '0x57ab1ec28d129707052df4df418d58a2d46d5f51', // SUSD | ||||
|     ], | ||||
| }; | ||||
|  | ||||
| export const ERC20_PROXY_ID = '0xf47261b0'; | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| import { MAINNET_CURVE_CONTRACTS } from './constants'; | ||||
|  | ||||
| // tslint:disable completed-docs | ||||
| export function getCurveAddressesForPair(takerToken: string, makerToken: string): string[] { | ||||
|     return Object.keys(MAINNET_CURVE_CONTRACTS).filter(a => | ||||
|         [makerToken, takerToken].every(t => MAINNET_CURVE_CONTRACTS[a].includes(t)), | ||||
|     ); | ||||
| } | ||||
| @@ -4,15 +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, | ||||
|     Fill, | ||||
|     FillFlags, | ||||
|     NativeCollapsedFill, | ||||
|     NativeFillData, | ||||
| } from './types'; | ||||
| import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types'; | ||||
|  | ||||
| // tslint:disable: prefer-for-of no-bitwise completed-docs | ||||
|  | ||||
| @@ -26,7 +18,7 @@ export function createFillPaths(opts: { | ||||
|     targetInput?: BigNumber; | ||||
|     ethToOutputRate?: BigNumber; | ||||
|     excludedSources?: ERC20BridgeSource[]; | ||||
|     feeSchedule?: { [source: string]: BigNumber }; | ||||
|     feeSchedule?: FeeSchedule; | ||||
| }): Fill[][] { | ||||
|     const { side } = opts; | ||||
|     const excludedSources = opts.excludedSources || []; | ||||
| @@ -62,7 +54,7 @@ function nativeOrdersToPath( | ||||
|     orders: SignedOrderWithFillableAmounts[], | ||||
|     targetInput: BigNumber = POSITIVE_INF, | ||||
|     ethToOutputRate: BigNumber, | ||||
|     fees: { [source: string]: BigNumber }, | ||||
|     fees: FeeSchedule, | ||||
| ): Fill[] { | ||||
|     // Create a single path from all orders. | ||||
|     let path: Fill[] = []; | ||||
| @@ -71,7 +63,9 @@ function nativeOrdersToPath( | ||||
|         const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); | ||||
|         const input = side === MarketOperation.Sell ? takerAmount : makerAmount; | ||||
|         const output = side === MarketOperation.Sell ? makerAmount : takerAmount; | ||||
|         const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.Native] || 0); | ||||
|         const penalty = ethToOutputRate.times( | ||||
|             fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(), | ||||
|         ); | ||||
|         const rate = makerAmount.div(takerAmount); | ||||
|         // targetInput can be less than the order size | ||||
|         // whilst the penalty is constant, it affects the adjusted output | ||||
| @@ -116,7 +110,7 @@ function dexQuotesToPaths( | ||||
|     side: MarketOperation, | ||||
|     dexQuotes: DexSample[][], | ||||
|     ethToOutputRate: BigNumber, | ||||
|     fees: { [source: string]: BigNumber }, | ||||
|     fees: FeeSchedule, | ||||
| ): Fill[][] { | ||||
|     const paths: Fill[][] = []; | ||||
|     for (let quote of dexQuotes) { | ||||
| @@ -129,12 +123,13 @@ function dexQuotesToPaths( | ||||
|         for (let i = 0; i < quote.length; i++) { | ||||
|             const sample = quote[i]; | ||||
|             const prevSample = i === 0 ? undefined : quote[i - 1]; | ||||
|             const source = sample.source; | ||||
|             const { source, fillData } = sample; | ||||
|             const input = sample.input.minus(prevSample ? prevSample.input : 0); | ||||
|             const output = sample.output.minus(prevSample ? prevSample.output : 0); | ||||
|             const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData); | ||||
|             const penalty = | ||||
|                 i === 0 // Only the first fill in a DEX path incurs a penalty. | ||||
|                     ? ethToOutputRate.times(fees[source] || 0) | ||||
|                     ? ethToOutputRate.times(fee) | ||||
|                     : ZERO_AMOUNT; | ||||
|             const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||
|             const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output); | ||||
| @@ -147,6 +142,7 @@ function dexQuotesToPaths( | ||||
|                 adjustedRate, | ||||
|                 adjustedOutput, | ||||
|                 source, | ||||
|                 fillData, | ||||
|                 index: i, | ||||
|                 parent: i !== 0 ? path[path.length - 1] : undefined, | ||||
|                 flags: sourceToFillFlags(source), | ||||
| @@ -241,7 +237,7 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_ | ||||
| } | ||||
|  | ||||
| export function collapsePath(path: Fill[]): CollapsedFill[] { | ||||
|     const collapsed: Array<CollapsedFill | NativeCollapsedFill> = []; | ||||
|     const collapsed: CollapsedFill[] = []; | ||||
|     for (const fill of path) { | ||||
|         const source = fill.source; | ||||
|         if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { | ||||
| @@ -256,10 +252,10 @@ export function collapsePath(path: Fill[]): CollapsedFill[] { | ||||
|         } | ||||
|         collapsed.push({ | ||||
|             source: fill.source, | ||||
|             fillData: fill.fillData, | ||||
|             input: fill.input, | ||||
|             output: fill.output, | ||||
|             subFills: [fill], | ||||
|             nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined, | ||||
|         }); | ||||
|     } | ||||
|     return collapsed; | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import { | ||||
|     AggregationError, | ||||
|     DexSample, | ||||
|     ERC20BridgeSource, | ||||
|     FeeSchedule, | ||||
|     GetMarketOrdersOpts, | ||||
|     OptimizedMarketOrder, | ||||
|     OrderDomain, | ||||
| @@ -77,6 +78,7 @@ export class MarketOperationUtils { | ||||
|         } | ||||
|         const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; | ||||
|         const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); | ||||
|         const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase); | ||||
|  | ||||
|         // Call the sampler contract. | ||||
|         const samplerPromise = this._sampler.executeAsync( | ||||
| @@ -89,26 +91,32 @@ export class MarketOperationUtils { | ||||
|                 takerToken, | ||||
|             ), | ||||
|             // Get ETH -> maker token price. | ||||
|             DexOrderSampler.ops.getMedianSellRate( | ||||
|             await DexOrderSampler.ops.getMedianSellRateAsync( | ||||
|                 difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), | ||||
|                 makerToken, | ||||
|                 this._wethAddress, | ||||
|                 ONE_ETHER, | ||||
|                 this._wethAddress, | ||||
|                 this._sampler.balancerPoolsCache, | ||||
|                 this._liquidityProviderRegistry, | ||||
|                 this._multiBridge, | ||||
|             ), | ||||
|             // Get sell quotes for taker -> maker. | ||||
|             DexOrderSampler.ops.getSellQuotes( | ||||
|                 difference(SELL_SOURCES.concat(this._optionalSources()), _opts.excludedSources), | ||||
|             await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                 difference( | ||||
|                     SELL_SOURCES.concat(this._optionalSources()), | ||||
|                     _opts.excludedSources.concat(ERC20BridgeSource.Balancer), | ||||
|                 ), | ||||
|                 makerToken, | ||||
|                 takerToken, | ||||
|                 getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), | ||||
|                 sampleAmounts, | ||||
|                 this._wethAddress, | ||||
|                 this._sampler.balancerPoolsCache, | ||||
|                 this._liquidityProviderRegistry, | ||||
|                 this._multiBridge, | ||||
|             ), | ||||
|         ); | ||||
|  | ||||
|         const rfqtPromise = getRfqtIndicativeQuotesAsync( | ||||
|             nativeOrders[0].makerAssetData, | ||||
|             nativeOrders[0].takerAssetData, | ||||
| @@ -116,14 +124,29 @@ export class MarketOperationUtils { | ||||
|             takerAmount, | ||||
|             _opts, | ||||
|         ); | ||||
|  | ||||
|         const balancerPromise = this._sampler.executeAsync( | ||||
|             await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                 difference([ERC20BridgeSource.Balancer], _opts.excludedSources), | ||||
|                 makerToken, | ||||
|                 takerToken, | ||||
|                 sampleAmounts, | ||||
|                 this._wethAddress, | ||||
|                 this._sampler.balancerPoolsCache, | ||||
|                 this._liquidityProviderRegistry, | ||||
|                 this._multiBridge, | ||||
|             ), | ||||
|         ); | ||||
|  | ||||
|         const [ | ||||
|             [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes], | ||||
|             rfqtIndicativeQuotes, | ||||
|         ] = await Promise.all([samplerPromise, rfqtPromise]); | ||||
|             [balancerQuotes], | ||||
|         ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); | ||||
|         return this._generateOptimizedOrders({ | ||||
|             orderFillableAmounts, | ||||
|             nativeOrders, | ||||
|             dexQuotes, | ||||
|             dexQuotes: dexQuotes.concat(balancerQuotes), | ||||
|             rfqtIndicativeQuotes, | ||||
|             liquidityProviderAddress, | ||||
|             multiBridgeAddress: this._multiBridge, | ||||
| @@ -159,6 +182,8 @@ export class MarketOperationUtils { | ||||
|         } | ||||
|         const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; | ||||
|         const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); | ||||
|         const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); | ||||
|  | ||||
|         // Call the sampler contract. | ||||
|         const samplerPromise = this._sampler.executeAsync( | ||||
|             // Get native order fillable amounts. | ||||
| @@ -170,30 +195,45 @@ export class MarketOperationUtils { | ||||
|                 takerToken, | ||||
|             ), | ||||
|             // Get ETH -> taker token price. | ||||
|             DexOrderSampler.ops.getMedianSellRate( | ||||
|             await DexOrderSampler.ops.getMedianSellRateAsync( | ||||
|                 difference(FEE_QUOTE_SOURCES.concat(this._optionalSources()), _opts.excludedSources), | ||||
|                 takerToken, | ||||
|                 this._wethAddress, | ||||
|                 ONE_ETHER, | ||||
|                 this._wethAddress, | ||||
|                 this._sampler.balancerPoolsCache, | ||||
|                 this._liquidityProviderRegistry, | ||||
|                 this._multiBridge, | ||||
|             ), | ||||
|             // Get buy quotes for taker -> maker. | ||||
|             DexOrderSampler.ops.getBuyQuotes( | ||||
|             await DexOrderSampler.ops.getBuyQuotesAsync( | ||||
|                 difference( | ||||
|                     BUY_SOURCES.concat( | ||||
|                         this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [], | ||||
|                     ), | ||||
|                     _opts.excludedSources, | ||||
|                     _opts.excludedSources.concat(ERC20BridgeSource.Balancer), | ||||
|                 ), | ||||
|                 makerToken, | ||||
|                 takerToken, | ||||
|                 getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase), | ||||
|                 sampleAmounts, | ||||
|                 this._wethAddress, | ||||
|                 this._sampler.balancerPoolsCache, | ||||
|                 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, | ||||
|             ), | ||||
|         ); | ||||
|  | ||||
|         const rfqtPromise = getRfqtIndicativeQuotesAsync( | ||||
|             nativeOrders[0].makerAssetData, | ||||
|             nativeOrders[0].takerAssetData, | ||||
| @@ -204,12 +244,13 @@ export class MarketOperationUtils { | ||||
|         const [ | ||||
|             [orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes], | ||||
|             rfqtIndicativeQuotes, | ||||
|         ] = await Promise.all([samplerPromise, rfqtPromise]); | ||||
|             [balancerQuotes], | ||||
|         ] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); | ||||
|  | ||||
|         return this._generateOptimizedOrders({ | ||||
|             orderFillableAmounts, | ||||
|             nativeOrders, | ||||
|             dexQuotes, | ||||
|             dexQuotes: dexQuotes.concat(balancerQuotes), | ||||
|             rfqtIndicativeQuotes, | ||||
|             liquidityProviderAddress, | ||||
|             multiBridgeAddress: this._multiBridge, | ||||
| @@ -251,24 +292,30 @@ export class MarketOperationUtils { | ||||
|         const sources = difference(BUY_SOURCES, _opts.excludedSources); | ||||
|         const ops = [ | ||||
|             ...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)), | ||||
|             ...batchNativeOrders.map(orders => | ||||
|                 DexOrderSampler.ops.getMedianSellRate( | ||||
|                     difference(FEE_QUOTE_SOURCES, _opts.excludedSources), | ||||
|                     getNativeOrderTokens(orders[0])[1], | ||||
|                     this._wethAddress, | ||||
|                     ONE_ETHER, | ||||
|                     this._wethAddress, | ||||
|             ...(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, i) => | ||||
|                 DexOrderSampler.ops.getBuyQuotes( | ||||
|                     sources, | ||||
|                     getNativeOrderTokens(orders[0])[0], | ||||
|                     getNativeOrderTokens(orders[0])[1], | ||||
|                     [makerAmounts[i]], | ||||
|                     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, | ||||
|                     ), | ||||
|                 ), | ||||
|             ), | ||||
|             )), | ||||
|         ]; | ||||
|  | ||||
|         const executeResults = await this._sampler.executeBatchAsync(ops); | ||||
| @@ -325,7 +372,7 @@ export class MarketOperationUtils { | ||||
|         bridgeSlippage?: number; | ||||
|         maxFallbackSlippage?: number; | ||||
|         excludedSources?: ERC20BridgeSource[]; | ||||
|         feeSchedule?: { [source: string]: BigNumber }; | ||||
|         feeSchedule?: FeeSchedule; | ||||
|         allowFallback?: boolean; | ||||
|         shouldBatchBridgeOrders?: boolean; | ||||
|         liquidityProviderAddress?: string; | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| import { NULL_ADDRESS } from './constants'; | ||||
|  | ||||
| // tslint:disable completed-docs | ||||
|  | ||||
| export const TOKENS = { | ||||
|     WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', | ||||
|     DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|     USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|     MKR: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', | ||||
| }; | ||||
| // tslint:disable enum-naming | ||||
| enum Tokens { | ||||
|     WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', | ||||
|     DAI = '0x6b175474e89094c44da98b954eedeac495271d0f', | ||||
|     USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||||
|     MKR = '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', | ||||
| } | ||||
|  | ||||
| export function getMultiBridgeIntermediateToken(takerToken: string, makerToken: string): string { | ||||
|     let intermediateToken = NULL_ADDRESS; | ||||
|     if (takerToken !== TOKENS.WETH && makerToken !== TOKENS.WETH) { | ||||
|         intermediateToken = TOKENS.WETH; | ||||
|     } else if (takerToken === TOKENS.USDC || makerToken === TOKENS.USDC) { | ||||
|         intermediateToken = TOKENS.DAI; | ||||
|     if (takerToken !== Tokens.WETH && makerToken !== Tokens.WETH) { | ||||
|         intermediateToken = Tokens.WETH; | ||||
|     } else if (takerToken === Tokens.USDC || makerToken === Tokens.USDC) { | ||||
|         intermediateToken = Tokens.DAI; | ||||
|     } | ||||
|     return intermediateToken; | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; | ||||
| import { AbiEncoder, BigNumber } from '@0x/utils'; | ||||
|  | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; | ||||
| import { getCurveInfo } from '../source_utils'; | ||||
|  | ||||
| import { | ||||
|     ERC20_PROXY_ID, | ||||
| @@ -20,12 +19,15 @@ import { collapsePath } from './fills'; | ||||
| import { getMultiBridgeIntermediateToken } from './multibridge_utils'; | ||||
| import { | ||||
|     AggregationError, | ||||
|     BalancerFillData, | ||||
|     CollapsedFill, | ||||
|     CurveFillData, | ||||
|     ERC20BridgeSource, | ||||
|     Fill, | ||||
|     NativeCollapsedFill, | ||||
|     OptimizedMarketOrder, | ||||
|     OrderDomain, | ||||
|     UniswapV2FillData, | ||||
| } from './types'; | ||||
|  | ||||
| // tslint:disable completed-docs no-unnecessary-type-assertion | ||||
| @@ -151,7 +153,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts | ||||
|     const orders: OptimizedMarketOrder[] = []; | ||||
|     for (let i = 0; i < collapsedPath.length; ) { | ||||
|         if (collapsedPath[i].source === ERC20BridgeSource.Native) { | ||||
|             orders.push(createNativeOrder(collapsedPath[i])); | ||||
|             orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill)); | ||||
|             ++i; | ||||
|             continue; | ||||
|         } | ||||
| @@ -184,14 +186,11 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder | ||||
|         case ERC20BridgeSource.Uniswap: | ||||
|             return opts.contractAddresses.uniswapBridge; | ||||
|         case ERC20BridgeSource.UniswapV2: | ||||
|         case ERC20BridgeSource.UniswapV2Eth: | ||||
|             return opts.contractAddresses.uniswapV2Bridge; | ||||
|         case ERC20BridgeSource.CurveUsdcDai: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdt: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtTusd: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtBusd: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtSusd: | ||||
|         case ERC20BridgeSource.Curve: | ||||
|             return opts.contractAddresses.curveBridge; | ||||
|         case ERC20BridgeSource.Balancer: | ||||
|             return opts.contractAddresses.balancerBridge; | ||||
|         case ERC20BridgeSource.LiquidityProvider: | ||||
|             if (opts.liquidityProviderAddress === undefined) { | ||||
|                 throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.'); | ||||
| @@ -214,39 +213,33 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): | ||||
|  | ||||
|     let makerAssetData; | ||||
|     switch (fill.source) { | ||||
|         case ERC20BridgeSource.CurveUsdcDai: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdt: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtTusd: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtBusd: | ||||
|         case ERC20BridgeSource.CurveUsdcDaiUsdtSusd: | ||||
|             const { curveAddress, fromTokenIdx, toTokenIdx, version } = getCurveInfo( | ||||
|                 fill.source, | ||||
|                 takerToken, | ||||
|                 makerToken, | ||||
|             ); | ||||
|         case ERC20BridgeSource.Curve: | ||||
|             const curveFillData = (fill as CollapsedFill<CurveFillData>).fillData!; // tslint:disable-line:no-non-null-assertion | ||||
|             makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( | ||||
|                 makerToken, | ||||
|                 bridgeAddress, | ||||
|                 createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version), | ||||
|                 createCurveBridgeData( | ||||
|                     curveFillData.poolAddress, | ||||
|                     curveFillData.fromTokenIdx, | ||||
|                     curveFillData.toTokenIdx, | ||||
|                     1, // "version" | ||||
|                 ), | ||||
|             ); | ||||
|             break; | ||||
|         case ERC20BridgeSource.Balancer: | ||||
|             const balancerFillData = (fill as CollapsedFill<BalancerFillData>).fillData!; // tslint:disable-line:no-non-null-assertion | ||||
|             makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( | ||||
|                 makerToken, | ||||
|                 bridgeAddress, | ||||
|                 createBalancerBridgeData(takerToken, balancerFillData.poolAddress), | ||||
|             ); | ||||
|             break; | ||||
|         case ERC20BridgeSource.UniswapV2: | ||||
|             const uniswapV2FillData = (fill as CollapsedFill<UniswapV2FillData>).fillData!; // tslint:disable-line:no-non-null-assertion | ||||
|             makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( | ||||
|                 makerToken, | ||||
|                 bridgeAddress, | ||||
|                 createUniswapV2BridgeData([takerToken, makerToken]), | ||||
|             ); | ||||
|             break; | ||||
|         case ERC20BridgeSource.UniswapV2Eth: | ||||
|             if (opts.contractAddresses.etherToken === NULL_ADDRESS) { | ||||
|                 throw new Error( | ||||
|                     `Cannot create a ${ERC20BridgeSource.UniswapV2Eth.toString()} order without a WETH address`, | ||||
|                 ); | ||||
|             } | ||||
|             makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( | ||||
|                 makerToken, | ||||
|                 bridgeAddress, | ||||
|                 createUniswapV2BridgeData([takerToken, opts.contractAddresses.etherToken, makerToken]), | ||||
|                 createUniswapV2BridgeData(uniswapV2FillData.tokenAddressPath), | ||||
|             ); | ||||
|             break; | ||||
|         case ERC20BridgeSource.MultiBridge: | ||||
| @@ -338,6 +331,14 @@ function createMultiBridgeData(takerToken: string, makerToken: string): string { | ||||
|     return encoder.encode({ takerToken, intermediateToken }); | ||||
| } | ||||
|  | ||||
| function createBalancerBridgeData(takerToken: string, poolAddress: string): string { | ||||
|     const encoder = AbiEncoder.create([ | ||||
|         { name: 'takerToken', type: 'address' }, | ||||
|         { name: 'poolAddress', type: 'address' }, | ||||
|     ]); | ||||
|     return encoder.encode({ takerToken, poolAddress }); | ||||
| } | ||||
|  | ||||
| function createCurveBridgeData( | ||||
|     curveAddress: string, | ||||
|     fromTokenIdx: number, | ||||
| @@ -404,10 +405,10 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder { | ||||
| function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { | ||||
|     return { | ||||
|         fills: [fill], | ||||
|         ...(fill as NativeCollapsedFill).nativeOrder, | ||||
|         ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,11 +3,13 @@ import { BigNumber } from '@0x/utils'; | ||||
| import { MarketOperation } from '../../types'; | ||||
|  | ||||
| import { ZERO_AMOUNT } from './constants'; | ||||
| import { getPathSize, isValidPath } from './fills'; | ||||
| import { getPathAdjustedSize, getPathSize, isValidPath } from './fills'; | ||||
| import { Fill } from './types'; | ||||
|  | ||||
| // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs | ||||
|  | ||||
| const RUN_LIMIT_DECAY_FACTOR = 0.8; | ||||
|  | ||||
| /** | ||||
|  * Find the optimal mixture of paths that maximizes (for sells) or minimizes | ||||
|  * (for buys) output, while meeting the input requirement. | ||||
| @@ -16,11 +18,15 @@ export function findOptimalPath( | ||||
|     side: MarketOperation, | ||||
|     paths: Fill[][], | ||||
|     targetInput: BigNumber, | ||||
|     runLimit?: number, | ||||
|     runLimit: number = 2 ** 15, | ||||
| ): Fill[] | undefined { | ||||
|     let optimalPath = paths[0] || []; | ||||
|     for (const path of paths.slice(1)) { | ||||
|         optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit); | ||||
|     // Sort paths in descending order by adjusted output amount. | ||||
|     const sortedPaths = paths | ||||
|         .slice(0) | ||||
|         .sort((a, b) => getPathAdjustedSize(b, targetInput)[1].comparedTo(getPathAdjustedSize(a, targetInput)[1])); | ||||
|     let optimalPath = sortedPaths[0] || []; | ||||
|     for (const [i, path] of sortedPaths.slice(1).entries()) { | ||||
|         optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i); | ||||
|     } | ||||
|     return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; | ||||
| } | ||||
| @@ -30,7 +36,7 @@ function mixPaths( | ||||
|     pathA: Fill[], | ||||
|     pathB: Fill[], | ||||
|     targetInput: BigNumber, | ||||
|     maxSteps: number = 2 ** 15, | ||||
|     maxSteps: number, | ||||
| ): Fill[] { | ||||
|     let bestPath: Fill[] = []; | ||||
|     let bestPathInput = ZERO_AMOUNT; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
|  | ||||
| import { BalancerPoolsCache } from './balancer_utils'; | ||||
| import { samplerOperations } from './sampler_operations'; | ||||
| import { BatchedOperation } from './types'; | ||||
|  | ||||
| @@ -11,6 +12,9 @@ export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, e | ||||
|     const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i)); | ||||
|     const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); | ||||
|     const amounts = stepSizes.map((_s, i) => { | ||||
|         if (i === numSamples - 1) { | ||||
|             return maxFillAmount; | ||||
|         } | ||||
|         return maxFillAmount | ||||
|             .times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])) | ||||
|             .integerValue(BigNumber.ROUND_UP); | ||||
| @@ -29,11 +33,11 @@ export class DexOrderSampler { | ||||
|      * for use with `DexOrderSampler.executeAsync()`. | ||||
|      */ | ||||
|     public static ops = samplerOperations; | ||||
|     private readonly _samplerContract: IERC20BridgeSamplerContract; | ||||
|  | ||||
|     constructor(samplerContract: IERC20BridgeSamplerContract) { | ||||
|         this._samplerContract = samplerContract; | ||||
|     } | ||||
|     constructor( | ||||
|         private readonly _samplerContract: IERC20BridgeSamplerContract, | ||||
|         public balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), | ||||
|     ) {} | ||||
|  | ||||
|     /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,20 @@ | ||||
| import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; | ||||
| import { getCurveInfo, isCurveSource } from '../source_utils'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { DEFAULT_FAKE_BUY_OPTS } from './constants'; | ||||
| import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; | ||||
|  | ||||
| import { BalancerPool, BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils'; | ||||
| import { DEFAULT_FAKE_BUY_OPTS, MAINNET_CURVE_CONTRACTS } from './constants'; | ||||
| import { getCurveAddressesForPair } from './curve_utils'; | ||||
| import { getMultiBridgeIntermediateToken } from './multibridge_utils'; | ||||
| import { BatchedOperation, DexSample, FakeBuyOpts } from './types'; | ||||
| import { | ||||
|     BalancerFillData, | ||||
|     BatchedOperation, | ||||
|     CurveFillData, | ||||
|     DexSample, | ||||
|     FakeBuyOpts, | ||||
|     SourceQuoteOperation, | ||||
|     UniswapV2FillData, | ||||
| } from './types'; | ||||
|  | ||||
| /** | ||||
|  * Composable operations that can be batched in a single transaction, | ||||
| @@ -34,12 +45,9 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getKyberSellQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     getKyberSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Kyber, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromKyberNetwork(takerToken, makerToken, takerFillAmounts) | ||||
| @@ -55,8 +63,9 @@ export const samplerOperations = { | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|         fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Kyber, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts, fakeBuyOpts) | ||||
| @@ -67,12 +76,9 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getUniswapSellQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     getUniswapSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Uniswap, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromUniswap(takerToken, makerToken, takerFillAmounts) | ||||
| @@ -83,12 +89,9 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getUniswapBuyQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     getUniswapBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Uniswap, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromUniswap(takerToken, makerToken, makerFillAmounts) | ||||
| @@ -99,8 +102,13 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getUniswapV2SellQuotes(tokenAddressPath: string[], takerFillAmounts: BigNumber[]): BatchedOperation<BigNumber[]> { | ||||
|     getUniswapV2SellQuotes( | ||||
|         tokenAddressPath: string[], | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): SourceQuoteOperation<UniswapV2FillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.UniswapV2, | ||||
|             fillData: { tokenAddressPath }, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromUniswapV2(tokenAddressPath, takerFillAmounts) | ||||
| @@ -111,8 +119,13 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getUniswapV2BuyQuotes(tokenAddressPath: string[], makerFillAmounts: BigNumber[]): BatchedOperation<BigNumber[]> { | ||||
|     getUniswapV2BuyQuotes( | ||||
|         tokenAddressPath: string[], | ||||
|         makerFillAmounts: BigNumber[], | ||||
|     ): SourceQuoteOperation<UniswapV2FillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.UniswapV2, | ||||
|             fillData: { tokenAddressPath }, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromUniswapV2(tokenAddressPath, makerFillAmounts) | ||||
| @@ -128,8 +141,9 @@ export const samplerOperations = { | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.LiquidityProvider, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, takerFillAmounts) | ||||
| @@ -143,41 +157,15 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getMultiBridgeSellQuotes( | ||||
|         multiBridgeAddress: string, | ||||
|         makerToken: string, | ||||
|         intermediateToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|         return { | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromMultiBridge( | ||||
|                         multiBridgeAddress, | ||||
|                         takerToken, | ||||
|                         intermediateToken, | ||||
|                         makerToken, | ||||
|                         takerFillAmounts, | ||||
|                     ) | ||||
|                     .getABIEncodedTransactionData(); | ||||
|             }, | ||||
|             handleCallResultsAsync: async (contract, callResults) => { | ||||
|                 return contract.getABIDecodedReturnData<BigNumber[]>( | ||||
|                     'sampleSellsFromLiquidityProviderRegistry', | ||||
|                     callResults, | ||||
|                 ); | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getLiquidityProviderBuyQuotes( | ||||
|         registryAddress: string, | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|         fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.LiquidityProvider, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromLiquidityProviderRegistry( | ||||
| @@ -197,12 +185,34 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getEth2DaiSellQuotes( | ||||
|     getMultiBridgeSellQuotes( | ||||
|         multiBridgeAddress: string, | ||||
|         makerToken: string, | ||||
|         intermediateToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.MultiBridge, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromMultiBridge( | ||||
|                         multiBridgeAddress, | ||||
|                         takerToken, | ||||
|                         intermediateToken, | ||||
|                         makerToken, | ||||
|                         takerFillAmounts, | ||||
|                     ) | ||||
|                     .getABIEncodedTransactionData(); | ||||
|             }, | ||||
|             handleCallResultsAsync: async (contract, callResults) => { | ||||
|                 return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromMultiBridge', callResults); | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getEth2DaiSellQuotes(makerToken: string, takerToken: string, takerFillAmounts: BigNumber[]): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Eth2Dai, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromEth2Dai(takerToken, makerToken, takerFillAmounts) | ||||
| @@ -213,12 +223,9 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getEth2DaiBuyQuotes( | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     getEth2DaiBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Eth2Dai, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromEth2Dai(takerToken, makerToken, makerFillAmounts) | ||||
| @@ -234,8 +241,14 @@ export const samplerOperations = { | ||||
|         fromTokenIdx: number, | ||||
|         toTokenIdx: number, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation<CurveFillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Curve, | ||||
|             fillData: { | ||||
|                 poolAddress: curveAddress, | ||||
|                 fromTokenIdx, | ||||
|                 toTokenIdx, | ||||
|             }, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleSellsFromCurve( | ||||
| @@ -256,8 +269,14 @@ export const samplerOperations = { | ||||
|         fromTokenIdx: number, | ||||
|         toTokenIdx: number, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|     ): BatchedOperation<BigNumber[]> { | ||||
|     ): SourceQuoteOperation<CurveFillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Curve, | ||||
|             fillData: { | ||||
|                 poolAddress: curveAddress, | ||||
|                 fromTokenIdx, | ||||
|                 toTokenIdx, | ||||
|             }, | ||||
|             encodeCall: contract => { | ||||
|                 return contract | ||||
|                     .sampleBuysFromCurve( | ||||
| @@ -273,24 +292,40 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getMedianSellRate( | ||||
|     getBalancerSellQuotes(pool: BalancerPool, takerFillAmounts: BigNumber[]): SourceQuoteOperation<BalancerFillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Balancer, | ||||
|             fillData: { poolAddress: pool.id }, | ||||
|             ...samplerOperations.constant(takerFillAmounts.map(amount => computeBalancerSellQuote(pool, amount))), | ||||
|         }; | ||||
|     }, | ||||
|     getBalancerBuyQuotes(pool: BalancerPool, makerFillAmounts: BigNumber[]): SourceQuoteOperation<BalancerFillData> { | ||||
|         return { | ||||
|             source: ERC20BridgeSource.Balancer, | ||||
|             fillData: { poolAddress: pool.id }, | ||||
|             ...samplerOperations.constant(makerFillAmounts.map(amount => computeBalancerBuyQuote(pool, amount))), | ||||
|         }; | ||||
|     }, | ||||
|     getMedianSellRateAsync: async ( | ||||
|         sources: ERC20BridgeSource[], | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmount: BigNumber, | ||||
|         wethAddress: string, | ||||
|         balancerPoolsCache?: BalancerPoolsCache, | ||||
|         liquidityProviderRegistryAddress?: string, | ||||
|         multiBridgeAddress?: string, | ||||
|     ): BatchedOperation<BigNumber> { | ||||
|     ): Promise<BatchedOperation<BigNumber>> => { | ||||
|         if (makerToken.toLowerCase() === takerToken.toLowerCase()) { | ||||
|             return samplerOperations.constant(new BigNumber(1)); | ||||
|         } | ||||
|         const getSellQuotes = samplerOperations.getSellQuotes( | ||||
|         const getSellQuotes = await samplerOperations.getSellQuotesAsync( | ||||
|             sources, | ||||
|             makerToken, | ||||
|             takerToken, | ||||
|             [takerFillAmount], | ||||
|             wethAddress, | ||||
|             balancerPoolsCache, | ||||
|             liquidityProviderRegistryAddress, | ||||
|             multiBridgeAddress, | ||||
|         ); | ||||
| @@ -343,181 +378,221 @@ export const samplerOperations = { | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getSellQuotes( | ||||
|     getSellQuotesAsync: async ( | ||||
|         sources: ERC20BridgeSource[], | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         takerFillAmounts: BigNumber[], | ||||
|         wethAddress: string, | ||||
|         balancerPoolsCache?: BalancerPoolsCache, | ||||
|         liquidityProviderRegistryAddress?: string, | ||||
|         multiBridgeAddress?: string, | ||||
|     ): BatchedOperation<DexSample[][]> { | ||||
|         const subOps = sources | ||||
|             .map(source => { | ||||
|                 let batchedOperation; | ||||
|                 if (source === ERC20BridgeSource.Eth2Dai) { | ||||
|                     batchedOperation = samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts); | ||||
|                 } else if (source === ERC20BridgeSource.Uniswap) { | ||||
|                     batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); | ||||
|                 } else if (source === ERC20BridgeSource.UniswapV2) { | ||||
|                     batchedOperation = samplerOperations.getUniswapV2SellQuotes( | ||||
|                         [takerToken, makerToken], | ||||
|                         takerFillAmounts, | ||||
|                     ); | ||||
|                 } else if (source === ERC20BridgeSource.UniswapV2Eth) { | ||||
|                     batchedOperation = samplerOperations.getUniswapV2SellQuotes( | ||||
|                         [takerToken, wethAddress, makerToken], | ||||
|                         takerFillAmounts, | ||||
|                     ); | ||||
|                 } else if (source === ERC20BridgeSource.Kyber) { | ||||
|                     batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); | ||||
|                 } else if (isCurveSource(source)) { | ||||
|                     const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken); | ||||
|                     if (fromTokenIdx !== -1 && toTokenIdx !== -1) { | ||||
|                         batchedOperation = samplerOperations.getCurveSellQuotes( | ||||
|                             curveAddress, | ||||
|                             fromTokenIdx, | ||||
|                             toTokenIdx, | ||||
|                             takerFillAmounts, | ||||
|                         ); | ||||
|                     } | ||||
|                 } else if (source === ERC20BridgeSource.LiquidityProvider) { | ||||
|                     if (liquidityProviderRegistryAddress === undefined) { | ||||
|                         throw new Error( | ||||
|                             'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', | ||||
|                         ); | ||||
|                     } | ||||
|                     batchedOperation = samplerOperations.getLiquidityProviderSellQuotes( | ||||
|                         liquidityProviderRegistryAddress, | ||||
|                         makerToken, | ||||
|                         takerToken, | ||||
|                         takerFillAmounts, | ||||
|                     ); | ||||
|                 } else if (source === ERC20BridgeSource.MultiBridge) { | ||||
|                     if (multiBridgeAddress === undefined) { | ||||
|                         throw new Error('Cannot sample liquidity from MultiBridge if an address is not provided.'); | ||||
|                     } | ||||
|                     const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); | ||||
|                     batchedOperation = samplerOperations.getMultiBridgeSellQuotes( | ||||
|                         multiBridgeAddress, | ||||
|                         makerToken, | ||||
|                         intermediateToken, | ||||
|                         takerToken, | ||||
|                         takerFillAmounts, | ||||
|                     ); | ||||
|                 } else { | ||||
|                     throw new Error(`Unsupported sell sample source: ${source}`); | ||||
|                 } | ||||
|                 return { batchedOperation, source }; | ||||
|             }) | ||||
|             .filter(op => op.batchedOperation) as Array<{ | ||||
|             batchedOperation: BatchedOperation<BigNumber[]>; | ||||
|             source: ERC20BridgeSource; | ||||
|         }>; | ||||
|     ): Promise<BatchedOperation<DexSample[][]>> => { | ||||
|         const subOps = _.flatten( | ||||
|             await Promise.all( | ||||
|                 sources.map( | ||||
|                     async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => { | ||||
|                         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 getCurveAddressesForPair(takerToken, makerToken).map(curveAddress => | ||||
|                                     samplerOperations.getCurveSellQuotes( | ||||
|                                         curveAddress, | ||||
|                                         MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken), | ||||
|                                         MAINNET_CURVE_CONTRACTS[curveAddress].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), | ||||
|                                 ); | ||||
|                             default: | ||||
|                                 throw new Error(`Unsupported sell sample source: ${source}`); | ||||
|                         } | ||||
|                     }, | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|         const samplerOps = subOps.filter(op => op.source !== ERC20BridgeSource.Balancer); | ||||
|         const nonSamplerOps = subOps.filter(op => op.source === ERC20BridgeSource.Balancer); | ||||
|         return { | ||||
|             encodeCall: contract => { | ||||
|                 const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract)); | ||||
|                 const subCalls = samplerOps.map(op => op.encodeCall(contract)); | ||||
|                 return contract.batchCall(subCalls).getABIEncodedTransactionData(); | ||||
|             }, | ||||
|             handleCallResultsAsync: async (contract, callResults) => { | ||||
|                 const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults); | ||||
|                 const samples = await Promise.all( | ||||
|                     subOps.map(async (op, i) => | ||||
|                         op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]), | ||||
|                     ), | ||||
|                 let samples = await Promise.all( | ||||
|                     samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), | ||||
|                 ); | ||||
|                 return subOps.map((op, i) => { | ||||
|                 samples = samples.concat( | ||||
|                     await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))), | ||||
|                 ); | ||||
|                 return [...samplerOps, ...nonSamplerOps].map((op, i) => { | ||||
|                     return samples[i].map((output, j) => ({ | ||||
|                         source: op.source, | ||||
|                         output, | ||||
|                         input: takerFillAmounts[j], | ||||
|                         fillData: op.fillData, | ||||
|                     })); | ||||
|                 }); | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     getBuyQuotes( | ||||
|     getBuyQuotesAsync: async ( | ||||
|         sources: ERC20BridgeSource[], | ||||
|         makerToken: string, | ||||
|         takerToken: string, | ||||
|         makerFillAmounts: BigNumber[], | ||||
|         wethAddress: string, | ||||
|         balancerPoolsCache?: BalancerPoolsCache, | ||||
|         liquidityProviderRegistryAddress?: string, | ||||
|         fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, | ||||
|     ): BatchedOperation<DexSample[][]> { | ||||
|         const subOps = sources | ||||
|             .map(source => { | ||||
|                 let batchedOperation; | ||||
|                 if (source === ERC20BridgeSource.Eth2Dai) { | ||||
|                     batchedOperation = samplerOperations.getEth2DaiBuyQuotes(makerToken, takerToken, makerFillAmounts); | ||||
|                 } else if (source === ERC20BridgeSource.Uniswap) { | ||||
|                     batchedOperation = samplerOperations.getUniswapBuyQuotes(makerToken, takerToken, makerFillAmounts); | ||||
|                 } else if (source === ERC20BridgeSource.UniswapV2) { | ||||
|                     batchedOperation = samplerOperations.getUniswapV2BuyQuotes( | ||||
|                         [takerToken, makerToken], | ||||
|                         makerFillAmounts, | ||||
|                     ); | ||||
|                 } else if (source === ERC20BridgeSource.UniswapV2Eth) { | ||||
|                     batchedOperation = samplerOperations.getUniswapV2BuyQuotes( | ||||
|                         [takerToken, wethAddress, makerToken], | ||||
|                         makerFillAmounts, | ||||
|                     ); | ||||
|                 } else if (source === ERC20BridgeSource.Kyber) { | ||||
|                     batchedOperation = samplerOperations.getKyberBuyQuotes( | ||||
|                         makerToken, | ||||
|                         takerToken, | ||||
|                         makerFillAmounts, | ||||
|                         fakeBuyOpts, | ||||
|                     ); | ||||
|                 } else if (isCurveSource(source)) { | ||||
|                     const { curveAddress, fromTokenIdx, toTokenIdx } = getCurveInfo(source, takerToken, makerToken); | ||||
|                     if (fromTokenIdx !== -1 && toTokenIdx !== -1) { | ||||
|                         batchedOperation = samplerOperations.getCurveBuyQuotes( | ||||
|                             curveAddress, | ||||
|                             fromTokenIdx, | ||||
|                             toTokenIdx, | ||||
|                             makerFillAmounts, | ||||
|                         ); | ||||
|                     } | ||||
|                 } else if (source === ERC20BridgeSource.LiquidityProvider) { | ||||
|                     if (liquidityProviderRegistryAddress === undefined) { | ||||
|                         throw new Error( | ||||
|                             'Cannot sample liquidity from a LiquidityProvider liquidity pool, if a registry is not provided.', | ||||
|                         ); | ||||
|                     } | ||||
|                     batchedOperation = samplerOperations.getLiquidityProviderBuyQuotes( | ||||
|                         liquidityProviderRegistryAddress, | ||||
|                         makerToken, | ||||
|                         takerToken, | ||||
|                         makerFillAmounts, | ||||
|                         fakeBuyOpts, | ||||
|                     ); | ||||
|                 } else { | ||||
|                     throw new Error(`Unsupported buy sample source: ${source}`); | ||||
|                 } | ||||
|                 return { source, batchedOperation }; | ||||
|             }) | ||||
|             .filter(op => op.batchedOperation) as Array<{ | ||||
|             batchedOperation: BatchedOperation<BigNumber[]>; | ||||
|             source: ERC20BridgeSource; | ||||
|         }>; | ||||
|     ): Promise<BatchedOperation<DexSample[][]>> => { | ||||
|         const subOps = _.flatten( | ||||
|             await Promise.all( | ||||
|                 sources.map( | ||||
|                     async (source): Promise<SourceQuoteOperation | SourceQuoteOperation[]> => { | ||||
|                         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, | ||||
|                                     fakeBuyOpts, | ||||
|                                 ); | ||||
|                             case ERC20BridgeSource.Curve: | ||||
|                                 return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress => | ||||
|                                     samplerOperations.getCurveBuyQuotes( | ||||
|                                         curveAddress, | ||||
|                                         MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken), | ||||
|                                         MAINNET_CURVE_CONTRACTS[curveAddress].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, | ||||
|                                     fakeBuyOpts, | ||||
|                                 ); | ||||
|                             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), | ||||
|                                 ); | ||||
|                             default: | ||||
|                                 throw new Error(`Unsupported buy sample source: ${source}`); | ||||
|                         } | ||||
|                     }, | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|         const samplerOps = subOps.filter(op => op.source !== ERC20BridgeSource.Balancer); | ||||
|         const nonSamplerOps = subOps.filter(op => op.source === ERC20BridgeSource.Balancer); | ||||
|         return { | ||||
|             encodeCall: contract => { | ||||
|                 const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract)); | ||||
|                 const subCalls = samplerOps.map(op => op.encodeCall(contract)); | ||||
|                 return contract.batchCall(subCalls).getABIEncodedTransactionData(); | ||||
|             }, | ||||
|             handleCallResultsAsync: async (contract, callResults) => { | ||||
|                 const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults); | ||||
|                 const samples = await Promise.all( | ||||
|                     subOps.map(async (op, i) => | ||||
|                         op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]), | ||||
|                     ), | ||||
|                 let samples = await Promise.all( | ||||
|                     samplerOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])), | ||||
|                 ); | ||||
|                 return subOps.map((op, i) => { | ||||
|                 samples = samples.concat( | ||||
|                     await Promise.all(nonSamplerOps.map(async op => op.handleCallResultsAsync(contract, ''))), | ||||
|                 ); | ||||
|                 return [...samplerOps, ...nonSamplerOps].map((op, i) => { | ||||
|                     return samples[i].map((output, j) => ({ | ||||
|                         source: op.source, | ||||
|                         output, | ||||
|                         input: makerFillAmounts[j], | ||||
|                         fillData: op.fillData, | ||||
|                     })); | ||||
|                 }); | ||||
|             }, | ||||
|   | ||||
| @@ -29,16 +29,12 @@ export enum ERC20BridgeSource { | ||||
|     Native = 'Native', | ||||
|     Uniswap = 'Uniswap', | ||||
|     UniswapV2 = 'Uniswap_V2', | ||||
|     UniswapV2Eth = 'Uniswap_V2_ETH', | ||||
|     Eth2Dai = 'Eth2Dai', | ||||
|     Kyber = 'Kyber', | ||||
|     CurveUsdcDai = 'Curve_USDC_DAI', | ||||
|     CurveUsdcDaiUsdt = 'Curve_USDC_DAI_USDT', | ||||
|     CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD', | ||||
|     CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD', | ||||
|     CurveUsdcDaiUsdtSusd = 'Curve_USDC_DAI_USDT_SUSD', | ||||
|     Curve = 'Curve', | ||||
|     LiquidityProvider = 'LiquidityProvider', | ||||
|     MultiBridge = 'MultiBridge', | ||||
|     Balancer = 'Balancer', | ||||
| } | ||||
|  | ||||
| // Internal `fillData` field for `Fill` objects. | ||||
| @@ -49,13 +45,28 @@ export interface NativeFillData extends FillData { | ||||
|     order: SignedOrderWithFillableAmounts; | ||||
| } | ||||
|  | ||||
| export interface CurveFillData extends FillData { | ||||
|     poolAddress: string; | ||||
|     fromTokenIdx: number; | ||||
|     toTokenIdx: number; | ||||
| } | ||||
|  | ||||
| export interface BalancerFillData extends FillData { | ||||
|     poolAddress: string; | ||||
| } | ||||
|  | ||||
| export interface UniswapV2FillData extends FillData { | ||||
|     tokenAddressPath: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Represents an individual DEX sample from the sampler contract. | ||||
|  */ | ||||
| export interface DexSample { | ||||
| export interface DexSample<TFillData extends FillData = FillData> { | ||||
|     source: ERC20BridgeSource; | ||||
|     input: BigNumber; | ||||
|     output: BigNumber; | ||||
|     fillData?: TFillData; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -71,7 +82,7 @@ export enum FillFlags { | ||||
| /** | ||||
|  * Represents a node on a fill path. | ||||
|  */ | ||||
| export interface Fill { | ||||
| export interface Fill<TFillData extends FillData = FillData> { | ||||
|     // See `FillFlags`. | ||||
|     flags: FillFlags; | ||||
|     // Input fill amount (taker asset amount in a sell, maker asset amount in a buy). | ||||
| @@ -92,13 +103,13 @@ export interface Fill { | ||||
|     source: ERC20BridgeSource; | ||||
|     // Data associated with this this Fill object. Used to reconstruct orders | ||||
|     // from paths. | ||||
|     fillData?: FillData | NativeFillData; | ||||
|     fillData?: TFillData; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Represents continguous fills on a path that have been merged together. | ||||
|  */ | ||||
| export interface CollapsedFill { | ||||
| export interface CollapsedFill<TFillData extends FillData = FillData> { | ||||
|     /** | ||||
|      * The source DEX. | ||||
|      */ | ||||
| @@ -118,14 +129,14 @@ export interface CollapsedFill { | ||||
|         input: BigNumber; | ||||
|         output: BigNumber; | ||||
|     }>; | ||||
|  | ||||
|     fillData?: TFillData; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A `CollapsedFill` wrapping a native order. | ||||
|  */ | ||||
| export interface NativeCollapsedFill extends CollapsedFill { | ||||
|     nativeOrder: SignedOrderWithFillableAmounts; | ||||
| } | ||||
| export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {} | ||||
|  | ||||
| /** | ||||
|  * Optimized orders to fill. | ||||
| @@ -141,6 +152,9 @@ export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts { | ||||
|     quoteRequestor?: QuoteRequestor; | ||||
| } | ||||
|  | ||||
| export type FeeEstimate = (fillData?: FillData) => number | BigNumber; | ||||
| export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; | ||||
|  | ||||
| /** | ||||
|  * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. | ||||
|  */ | ||||
| @@ -184,11 +198,11 @@ export interface GetMarketOrdersOpts { | ||||
|     /** | ||||
|      * Fees for each liquidity source, expressed in gas. | ||||
|      */ | ||||
|     feeSchedule: { [source: string]: BigNumber }; | ||||
|     feeSchedule: FeeSchedule; | ||||
|     /** | ||||
|      * Estimated gas consumed by each liquidity source. | ||||
|      */ | ||||
|     gasSchedule: { [source: string]: number }; | ||||
|     gasSchedule: FeeSchedule; | ||||
|     /** | ||||
|      * Whether to pad the quote with a redundant fallback quote using different | ||||
|      * sources. Defaults to `true`. | ||||
| @@ -210,6 +224,11 @@ export interface BatchedOperation<TResult> { | ||||
|     handleCallResultsAsync(contract: IERC20BridgeSamplerContract, callResults: string): Promise<TResult>; | ||||
| } | ||||
|  | ||||
| export interface SourceQuoteOperation<TFillData extends FillData = FillData> extends BatchedOperation<BigNumber[]> { | ||||
|     source: ERC20BridgeSource; | ||||
|     fillData?: TFillData; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Used in the ERC20BridgeSampler when a source does not natively | ||||
|  * support sampling via a specific buy amount. | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils'; | ||||
| import { constants } from '../constants'; | ||||
| import { MarketOperation } from '../types'; | ||||
|  | ||||
| import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; | ||||
| import { CollapsedFill, FeeSchedule, OptimizedMarketOrder } from './market_operation_utils/types'; | ||||
| import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; | ||||
|  | ||||
| const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; | ||||
| @@ -71,7 +71,7 @@ export interface QuoteFillInfo { | ||||
| } | ||||
|  | ||||
| export interface QuoteFillInfoOpts { | ||||
|     gasSchedule: { [soruce: string]: number }; | ||||
|     gasSchedule: FeeSchedule; | ||||
|     protocolFeeMultiplier: BigNumber; | ||||
| } | ||||
|  | ||||
| @@ -124,10 +124,7 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult | ||||
|             opts.gasSchedule, | ||||
|         ), | ||||
|         // Worst case gas and protocol fee is hitting all orders. | ||||
|         gas: getTotalGasUsedBySources( | ||||
|             getFlattenedFillsFromOrders(quoteInfo.orders).map(s => s.source), | ||||
|             opts.gasSchedule, | ||||
|         ), | ||||
|         gas: getTotalGasUsedByFills(getFlattenedFillsFromOrders(quoteInfo.orders), opts.gasSchedule), | ||||
|         protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length), | ||||
|     }; | ||||
|     return fromIntermediateQuoteFillResult(result, quoteInfo); | ||||
| @@ -137,7 +134,7 @@ export function fillQuoteOrders( | ||||
|     fillOrders: QuoteFillOrderCall[], | ||||
|     inputAmount: BigNumber, | ||||
|     protocolFeePerFillOrder: BigNumber, | ||||
|     gasSchedule: { [source: string]: number }, | ||||
|     gasSchedule: FeeSchedule, | ||||
| ): IntermediateQuoteFillResult { | ||||
|     const result: IntermediateQuoteFillResult = { | ||||
|         ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, | ||||
| @@ -152,8 +149,9 @@ export function fillQuoteOrders( | ||||
|             if (remainingInput.lte(0)) { | ||||
|                 break; | ||||
|             } | ||||
|             const { source } = fill; | ||||
|             result.gas += gasSchedule[source] || 0; | ||||
|             const { source, fillData } = fill; | ||||
|             const fee = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); | ||||
|             result.gas += new BigNumber(fee).toNumber(); | ||||
|             result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; | ||||
|  | ||||
|             // Actual rates are rarely linear, so fill subfills individually to | ||||
| @@ -347,10 +345,11 @@ export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): Col | ||||
|     return fills; | ||||
| } | ||||
|  | ||||
| function getTotalGasUsedBySources(sources: ERC20BridgeSource[], gasSchedule: { [source: string]: number }): number { | ||||
| function getTotalGasUsedByFills(fills: CollapsedFill[], gasSchedule: FeeSchedule): number { | ||||
|     let gasUsed = 0; | ||||
|     for (const s of sources) { | ||||
|         gasUsed += gasSchedule[s] || 0; | ||||
|     for (const f of fills) { | ||||
|         const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData); | ||||
|         gasUsed += new BigNumber(fee).toNumber(); | ||||
|     } | ||||
|     return gasUsed; | ||||
| } | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { DEFAULT_CURVE_OPTS } from './market_operation_utils/constants'; | ||||
| import { ERC20BridgeSource } from './market_operation_utils/types'; | ||||
|  | ||||
| export const isCurveSource = (source: ERC20BridgeSource): boolean => { | ||||
|     return Object.keys(DEFAULT_CURVE_OPTS).includes(source); | ||||
| }; | ||||
|  | ||||
| export const getCurveInfo = ( | ||||
|     source: ERC20BridgeSource, | ||||
|     takerToken: string, | ||||
|     makerToken: string, | ||||
| ): { curveAddress: string; fromTokenIdx: number; toTokenIdx: number; version: number } => { | ||||
|     const { curveAddress, tokens, version } = DEFAULT_CURVE_OPTS[source]; | ||||
|     const fromTokenIdx = tokens.indexOf(takerToken); | ||||
|     const toTokenIdx = tokens.indexOf(makerToken); | ||||
|     return { curveAddress, fromTokenIdx, toTokenIdx, version }; | ||||
| }; | ||||
| @@ -17,7 +17,7 @@ import { | ||||
|  | ||||
| import { MarketOperationUtils } from './market_operation_utils'; | ||||
| import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; | ||||
| import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; | ||||
| import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; | ||||
| import { isSupportedAssetDataInOrders } from './utils'; | ||||
|  | ||||
| import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; | ||||
| @@ -126,7 +126,9 @@ export class SwapQuoteCalculator { | ||||
|             // Scale fees by gas price. | ||||
|             const _opts: GetMarketOrdersOpts = { | ||||
|                 ...opts, | ||||
|                 feeSchedule: _.mapValues(opts.feeSchedule, v => v.times(gasPrice)), | ||||
|                 feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => | ||||
|                     gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), | ||||
|                 ), | ||||
|             }; | ||||
|  | ||||
|             const firstOrderMakerAssetData = !!prunedOrders[0] | ||||
| @@ -174,7 +176,7 @@ function createSwapQuote( | ||||
|     operation: MarketOperation, | ||||
|     assetFillAmount: BigNumber, | ||||
|     gasPrice: BigNumber, | ||||
|     gasSchedule: { [source: string]: number }, | ||||
|     gasSchedule: FeeSchedule, | ||||
| ): SwapQuote { | ||||
|     const bestCaseFillResult = simulateBestCaseFill({ | ||||
|         gasPrice, | ||||
|   | ||||
| @@ -12,9 +12,15 @@ import { SignedOrder } from '@0x/types'; | ||||
| import { BigNumber, hexUtils } from '@0x/utils'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { | ||||
|     BalancerPool, | ||||
|     computeBalancerBuyQuote, | ||||
|     computeBalancerSellQuote, | ||||
| } from '../src/utils/market_operation_utils/balancer_utils'; | ||||
| import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; | ||||
| import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; | ||||
| import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types'; | ||||
|  | ||||
| import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache'; | ||||
| import { MockSamplerContract } from './utils/mock_sampler_contract'; | ||||
|  | ||||
| const CHAIN_ID = 1; | ||||
| @@ -149,7 +155,7 @@ describe('DexSampler tests', () => { | ||||
|             const expectedTakerToken = randomAddress(); | ||||
|             const registry = randomAddress(); | ||||
|             const sampler = new MockSamplerContract({ | ||||
|                 sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => { | ||||
|                 sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { | ||||
|                     expect(registryAddress).to.eq(registry); | ||||
|                     expect(takerToken).to.eq(expectedTakerToken); | ||||
|                     expect(makerToken).to.eq(expectedMakerToken); | ||||
| @@ -158,12 +164,13 @@ describe('DexSampler tests', () => { | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const [result] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getSellQuotes( | ||||
|                 await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                     [ERC20BridgeSource.LiquidityProvider], | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     [toBaseUnitAmount(1000)], | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                     registry, | ||||
|                 ), | ||||
|             ); | ||||
| @@ -173,6 +180,7 @@ describe('DexSampler tests', () => { | ||||
|                         source: 'LiquidityProvider', | ||||
|                         output: toBaseUnitAmount(1001), | ||||
|                         input: toBaseUnitAmount(1000), | ||||
|                         fillData: undefined, | ||||
|                     }, | ||||
|                 ], | ||||
|             ]); | ||||
| @@ -183,7 +191,7 @@ describe('DexSampler tests', () => { | ||||
|             const expectedTakerToken = randomAddress(); | ||||
|             const registry = randomAddress(); | ||||
|             const sampler = new MockSamplerContract({ | ||||
|                 sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, fillAmounts) => { | ||||
|                 sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { | ||||
|                     expect(registryAddress).to.eq(registry); | ||||
|                     expect(takerToken).to.eq(expectedTakerToken); | ||||
|                     expect(makerToken).to.eq(expectedMakerToken); | ||||
| @@ -192,12 +200,13 @@ describe('DexSampler tests', () => { | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const [result] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getBuyQuotes( | ||||
|                 await DexOrderSampler.ops.getBuyQuotesAsync( | ||||
|                     [ERC20BridgeSource.LiquidityProvider], | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     [toBaseUnitAmount(1000)], | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                     registry, | ||||
|                 ), | ||||
|             ); | ||||
| @@ -207,6 +216,7 @@ describe('DexSampler tests', () => { | ||||
|                         source: 'LiquidityProvider', | ||||
|                         output: toBaseUnitAmount(999), | ||||
|                         input: toBaseUnitAmount(1000), | ||||
|                         fillData: undefined, | ||||
|                     }, | ||||
|                 ], | ||||
|             ]); | ||||
| @@ -233,12 +243,13 @@ describe('DexSampler tests', () => { | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const [result] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getSellQuotes( | ||||
|                 await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                     [ERC20BridgeSource.MultiBridge], | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     [toBaseUnitAmount(1000)], | ||||
|                     randomAddress(), | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                     randomAddress(), | ||||
|                     multiBridge, | ||||
|                 ), | ||||
| @@ -249,6 +260,7 @@ describe('DexSampler tests', () => { | ||||
|                         source: 'MultiBridge', | ||||
|                         output: toBaseUnitAmount(1001), | ||||
|                         input: toBaseUnitAmount(1000), | ||||
|                         fillData: undefined, | ||||
|                     }, | ||||
|                 ], | ||||
|             ]); | ||||
| @@ -412,72 +424,92 @@ describe('DexSampler tests', () => { | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); | ||||
|                 }, | ||||
|                 sampleSellsFromUniswapV2: (path, fillAmounts) => { | ||||
|                     expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); | ||||
|                     if (path.length === 2) { | ||||
|                         expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); | ||||
|                     } else if (path.length === 3) { | ||||
|                         expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]); | ||||
|                     } else { | ||||
|                         expect(path).to.have.lengthOf.within(2, 3); | ||||
|                     } | ||||
|                     expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue()); | ||||
|                 }, | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const [quotes] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getSellQuotes( | ||||
|                 await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                     sources, | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     expectedTakerFillAmounts, | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                 ), | ||||
|             ); | ||||
|             expect(quotes).to.be.length(sources.length); | ||||
|             const expectedQuotes = sources.map(s => | ||||
|                 expectedTakerFillAmounts.map(a => ({ | ||||
|                     source: s, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[s]).integerValue(), | ||||
|                     fillData: | ||||
|                         s === ERC20BridgeSource.UniswapV2 | ||||
|                             ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } | ||||
|                             : ((undefined as any) as FillData), | ||||
|                 })), | ||||
|             ); | ||||
|             expect(quotes).to.deep.eq(expectedQuotes); | ||||
|             const uniswapV2ETHQuotes = [ | ||||
|                 expectedTakerFillAmounts.map(a => ({ | ||||
|                     source: ERC20BridgeSource.UniswapV2, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(), | ||||
|                     fillData: { | ||||
|                         tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken], | ||||
|                     }, | ||||
|                 })), | ||||
|             ]; | ||||
|             //  extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB) | ||||
|             expect(quotes).to.have.lengthOf(sources.length + 1); | ||||
|             expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); | ||||
|         }); | ||||
|  | ||||
|         it('getSellQuotes() includes ETH for Uniswap_V2_ETH', async () => { | ||||
|         it('getSellQuotes() uses samples from Balancer', async () => { | ||||
|             const expectedTakerToken = randomAddress(); | ||||
|             const expectedMakerToken = randomAddress(); | ||||
|             const sources = [ERC20BridgeSource.UniswapV2Eth]; | ||||
|             const ratesBySource: RatesBySource = { | ||||
|                 [ERC20BridgeSource.UniswapV2Eth]: getRandomFloat(0, 100), | ||||
|             }; | ||||
|             const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); | ||||
|             const sampler = new MockSamplerContract({ | ||||
|                 sampleSellsFromUniswapV2: (path, fillAmounts) => { | ||||
|                     expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]); | ||||
|                     expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2Eth]).integerValue()); | ||||
|             const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()]; | ||||
|             const balancerPoolsCache = new MockBalancerPoolsCache({ | ||||
|                 getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { | ||||
|                     expect(takerToken).equal(expectedTakerToken); | ||||
|                     expect(makerToken).equal(expectedMakerToken); | ||||
|                     return Promise.resolve(pools); | ||||
|                 }, | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const dexOrderSampler = new DexOrderSampler(new MockSamplerContract({}), balancerPoolsCache); | ||||
|             const [quotes] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getSellQuotes( | ||||
|                     sources, | ||||
|                 await DexOrderSampler.ops.getSellQuotesAsync( | ||||
|                     [ERC20BridgeSource.Balancer], | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     expectedTakerFillAmounts, | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                 ), | ||||
|             ); | ||||
|             expect(quotes).to.be.length(sources.length); | ||||
|             const expectedQuotes = sources.map(s => | ||||
|             const expectedQuotes = pools.map(p => | ||||
|                 expectedTakerFillAmounts.map(a => ({ | ||||
|                     source: s, | ||||
|                     source: ERC20BridgeSource.Balancer, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[s]).integerValue(), | ||||
|                     output: computeBalancerSellQuote(p, a), | ||||
|                     fillData: { poolAddress: p.id }, | ||||
|                 })), | ||||
|             ); | ||||
|             expect(quotes).to.have.lengthOf(2); // one array per pool | ||||
|             expect(quotes).to.deep.eq(expectedQuotes); | ||||
|         }); | ||||
|  | ||||
|         it('getBuyQuotes()', async () => { | ||||
|             const expectedTakerToken = randomAddress(); | ||||
|             const expectedMakerToken = randomAddress(); | ||||
|             const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap]; | ||||
|             const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2]; | ||||
|             const ratesBySource: RatesBySource = { | ||||
|                 [ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100), | ||||
|                 [ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100), | ||||
| @@ -498,78 +530,85 @@ describe('DexSampler tests', () => { | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); | ||||
|                 }, | ||||
|                 sampleBuysFromUniswapV2: (path, fillAmounts) => { | ||||
|                     expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); | ||||
|                     if (path.length === 2) { | ||||
|                         expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]); | ||||
|                     } else if (path.length === 3) { | ||||
|                         expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]); | ||||
|                     } else { | ||||
|                         expect(path).to.have.lengthOf.within(2, 3); | ||||
|                     } | ||||
|                     expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue()); | ||||
|                 }, | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const [quotes] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getBuyQuotes( | ||||
|                 await DexOrderSampler.ops.getBuyQuotesAsync( | ||||
|                     sources, | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     expectedMakerFillAmounts, | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                 ), | ||||
|             ); | ||||
|             expect(quotes).to.be.length(sources.length); | ||||
|             const expectedQuotes = sources.map(s => | ||||
|                 expectedMakerFillAmounts.map(a => ({ | ||||
|                     source: s, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[s]).integerValue(), | ||||
|                     fillData: | ||||
|                         s === ERC20BridgeSource.UniswapV2 | ||||
|                             ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } | ||||
|                             : ((undefined as any) as FillData), | ||||
|                 })), | ||||
|             ); | ||||
|             expect(quotes).to.deep.eq(expectedQuotes); | ||||
|             const uniswapV2ETHQuotes = [ | ||||
|                 expectedMakerFillAmounts.map(a => ({ | ||||
|                     source: ERC20BridgeSource.UniswapV2, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(), | ||||
|                     fillData: { | ||||
|                         tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken], | ||||
|                     }, | ||||
|                 })), | ||||
|             ]; | ||||
|             //  extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB) | ||||
|             expect(quotes).to.have.lengthOf(sources.length + 1); | ||||
|             expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); | ||||
|         }); | ||||
|         it('getBuyQuotes() includes ETH for Uniswap_V2_ETH', async () => { | ||||
|         it('getBuyQuotes() uses samples from Balancer', async () => { | ||||
|             const expectedTakerToken = randomAddress(); | ||||
|             const expectedMakerToken = randomAddress(); | ||||
|             const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2Eth]; | ||||
|             const ratesBySource: RatesBySource = { | ||||
|                 [ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100), | ||||
|                 [ERC20BridgeSource.Uniswap]: getRandomFloat(0, 100), | ||||
|                 [ERC20BridgeSource.UniswapV2Eth]: getRandomFloat(0, 100), | ||||
|             }; | ||||
|             const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); | ||||
|             const sampler = new MockSamplerContract({ | ||||
|                 sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => { | ||||
|                     expect(takerToken).to.eq(expectedTakerToken); | ||||
|                     expect(makerToken).to.eq(expectedMakerToken); | ||||
|                     expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Uniswap]).integerValue()); | ||||
|                 }, | ||||
|                 sampleBuysFromEth2Dai: (takerToken, makerToken, fillAmounts) => { | ||||
|                     expect(takerToken).to.eq(expectedTakerToken); | ||||
|                     expect(makerToken).to.eq(expectedMakerToken); | ||||
|                     expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue()); | ||||
|                 }, | ||||
|                 sampleBuysFromUniswapV2: (path, fillAmounts) => { | ||||
|                     expect(path).to.deep.eq([expectedTakerToken, wethAddress, expectedMakerToken]); | ||||
|                     expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts); | ||||
|                     return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.UniswapV2Eth]).integerValue()); | ||||
|             const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()]; | ||||
|             const balancerPoolsCache = new MockBalancerPoolsCache({ | ||||
|                 getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { | ||||
|                     expect(takerToken).equal(expectedTakerToken); | ||||
|                     expect(makerToken).equal(expectedMakerToken); | ||||
|                     return Promise.resolve(pools); | ||||
|                 }, | ||||
|             }); | ||||
|             const dexOrderSampler = new DexOrderSampler(sampler); | ||||
|             const dexOrderSampler = new DexOrderSampler(new MockSamplerContract({}), balancerPoolsCache); | ||||
|             const [quotes] = await dexOrderSampler.executeAsync( | ||||
|                 DexOrderSampler.ops.getBuyQuotes( | ||||
|                     sources, | ||||
|                 await DexOrderSampler.ops.getBuyQuotesAsync( | ||||
|                     [ERC20BridgeSource.Balancer], | ||||
|                     expectedMakerToken, | ||||
|                     expectedTakerToken, | ||||
|                     expectedMakerFillAmounts, | ||||
|                     wethAddress, | ||||
|                     dexOrderSampler.balancerPoolsCache, | ||||
|                 ), | ||||
|             ); | ||||
|             expect(quotes).to.be.length(sources.length); | ||||
|             const expectedQuotes = sources.map(s => | ||||
|             const expectedQuotes = pools.map(p => | ||||
|                 expectedMakerFillAmounts.map(a => ({ | ||||
|                     source: s, | ||||
|                     source: ERC20BridgeSource.Balancer, | ||||
|                     input: a, | ||||
|                     output: a.times(ratesBySource[s]).integerValue(), | ||||
|                     output: computeBalancerBuyQuote(p, a), | ||||
|                     fillData: { poolAddress: p.id }, | ||||
|                 })), | ||||
|             ); | ||||
|             expect(quotes).to.have.lengthOf(2); //  one set per pool | ||||
|             expect(quotes).to.deep.eq(expectedQuotes); | ||||
|         }); | ||||
|     }); | ||||
| @@ -600,4 +639,14 @@ describe('DexSampler tests', () => { | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| function generateBalancerPool(): BalancerPool { | ||||
|     return { | ||||
|         id: randomAddress(), | ||||
|         balanceIn: getRandomInteger(1, 1e18), | ||||
|         balanceOut: getRandomInteger(1, 1e18), | ||||
|         weightIn: getRandomInteger(0, 1e5), | ||||
|         weightOut: getRandomInteger(0, 1e5), | ||||
|         swapFee: getRandomInteger(0, 1e5), | ||||
|     }; | ||||
| } | ||||
| // tslint:disable-next-line: max-file-line-count | ||||
|   | ||||
| @@ -16,16 +16,10 @@ import * as _ from 'lodash'; | ||||
|  | ||||
| import { MarketOperation, SignedOrderWithFillableAmounts } from '../src'; | ||||
| import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; | ||||
| import { | ||||
|     BUY_SOURCES, | ||||
|     DEFAULT_CURVE_OPTS, | ||||
|     POSITIVE_INF, | ||||
|     SELL_SOURCES, | ||||
|     ZERO_AMOUNT, | ||||
| } from '../src/utils/market_operation_utils/constants'; | ||||
| 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'; | ||||
| import { DexSample, ERC20BridgeSource, NativeFillData } from '../src/utils/market_operation_utils/types'; | ||||
| import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; | ||||
|  | ||||
| // tslint:disable: custom-no-magic-numbers | ||||
| describe('MarketOperationUtils tests', () => { | ||||
| @@ -93,10 +87,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|             case UNISWAP_V2_BRIDGE_ADDRESS.toLowerCase(): | ||||
|                 return ERC20BridgeSource.UniswapV2; | ||||
|             case CURVE_BRIDGE_ADDRESS.toLowerCase(): | ||||
|                 const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter( | ||||
|                     k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1, | ||||
|                 ); | ||||
|                 return curveSource[0] as ERC20BridgeSource; | ||||
|                 return ERC20BridgeSource.Curve; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
| @@ -132,12 +123,18 @@ describe('MarketOperationUtils tests', () => { | ||||
|         chainId: CHAIN_ID, | ||||
|     }; | ||||
|  | ||||
|     function createSamplesFromRates(source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[]): DexSample[] { | ||||
|     function createSamplesFromRates( | ||||
|         source: ERC20BridgeSource, | ||||
|         inputs: Numberish[], | ||||
|         rates: Numberish[], | ||||
|         fillData?: FillData, | ||||
|     ): DexSample[] { | ||||
|         const samples: DexSample[] = []; | ||||
|         inputs.forEach((input, i) => { | ||||
|             const rate = rates[i]; | ||||
|             samples.push({ | ||||
|                 source, | ||||
|                 fillData: fillData || DEFAULT_FILL_DATA[source], | ||||
|                 input: new BigNumber(input), | ||||
|                 output: new BigNumber(input) | ||||
|                     .minus(i === 0 ? 0 : samples[i - 1].input) | ||||
| @@ -161,10 +158,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|     function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { | ||||
|         return ( | ||||
|             sources: ERC20BridgeSource[], | ||||
|             makerToken: string, | ||||
|             takerToken: string, | ||||
|             _makerToken: string, | ||||
|             _takerToken: string, | ||||
|             fillAmounts: BigNumber[], | ||||
|             wethAddress: string, | ||||
|             _wethAddress: string, | ||||
|         ) => { | ||||
|             return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s])); | ||||
|         }; | ||||
| @@ -184,10 +181,11 @@ describe('MarketOperationUtils tests', () => { | ||||
|             takerToken: string, | ||||
|             fillAmounts: BigNumber[], | ||||
|             wethAddress: string, | ||||
|             _balancerPoolsCache?: any, | ||||
|             liquidityProviderAddress?: string, | ||||
|         ) => { | ||||
|             liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress; | ||||
|             liquidityPoolParams.sources = sources; | ||||
|             liquidityPoolParams.sources = liquidityPoolParams.sources.concat(sources); | ||||
|             return tradeOperation(rates)( | ||||
|                 sources, | ||||
|                 makerToken, | ||||
| @@ -203,10 +201,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|     function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { | ||||
|         return ( | ||||
|             sources: ERC20BridgeSource[], | ||||
|             makerToken: string, | ||||
|             takerToken: string, | ||||
|             _makerToken: string, | ||||
|             _takerToken: string, | ||||
|             fillAmounts: BigNumber[], | ||||
|             wethAddress: string, | ||||
|             _wethAddress: string, | ||||
|         ) => { | ||||
|             return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))); | ||||
|         }; | ||||
| @@ -229,18 +227,18 @@ describe('MarketOperationUtils tests', () => { | ||||
|  | ||||
|     function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { | ||||
|         return ( | ||||
|             sources: ERC20BridgeSource[], | ||||
|             makerToken: string, | ||||
|             takerToken: string, | ||||
|             fillAmounts: BigNumber[], | ||||
|             wethAddress: string, | ||||
|             _sources: ERC20BridgeSource[], | ||||
|             _makerToken: string, | ||||
|             _takerToken: string, | ||||
|             _fillAmounts: BigNumber[], | ||||
|             _wethAddress: string, | ||||
|         ) => { | ||||
|             return new BigNumber(rate); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation { | ||||
|         return (registryAddress: string, takerToken: string, makerToken: string): string => { | ||||
|         return (_registryAddress: string, _takerToken: string, _makerToken: string): string => { | ||||
|             return NULL_ADDRESS; | ||||
|         }; | ||||
|     } | ||||
| @@ -288,17 +286,23 @@ describe('MarketOperationUtils tests', () => { | ||||
|         [ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES), | ||||
|         [ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES), | ||||
|         [ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES), | ||||
|         [ERC20BridgeSource.UniswapV2]: createDecreasingRates(NUM_SAMPLES), | ||||
|         [ERC20BridgeSource.UniswapV2Eth]: createDecreasingRates(NUM_SAMPLES), | ||||
|         [ERC20BridgeSource.CurveUsdcDai]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.CurveUsdcDaiUsdt]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.UniswapV2]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.Balancer]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.Curve]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0), | ||||
|         [ERC20BridgeSource.MultiBridge]: _.times(NUM_SAMPLES, () => 0), | ||||
|     }; | ||||
|  | ||||
|     interface FillDataBySource { | ||||
|         [source: string]: FillData; | ||||
|     } | ||||
|  | ||||
|     const DEFAULT_FILL_DATA: FillDataBySource = { | ||||
|         [ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] }, | ||||
|         [ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() }, | ||||
|         [ERC20BridgeSource.Curve]: { poolAddress: randomAddress(), fromTokenIdx: 0, toTokenIdx: 1 }, | ||||
|     }; | ||||
|  | ||||
|     const DEFAULT_OPS = { | ||||
|         getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { | ||||
|             return orders.map(o => o.takerAssetAmount); | ||||
| @@ -306,9 +310,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|         getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { | ||||
|             return orders.map(o => o.makerAssetAmount); | ||||
|         }, | ||||
|         getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), | ||||
|         getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), | ||||
|         getMedianSellRate: createGetMedianSellRate(1), | ||||
|         getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), | ||||
|         getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), | ||||
|         getMedianSellRateAsync: createGetMedianSellRate(1), | ||||
|         getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(), | ||||
|     }; | ||||
|  | ||||
| @@ -346,11 +350,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 sampleDistributionBase: 1, | ||||
|                 bridgeSlippage: 0, | ||||
|                 maxFallbackSlippage: 100, | ||||
|                 excludedSources: [ | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.UniswapV2Eth, | ||||
|                     ...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]), | ||||
|                 ], | ||||
|                 excludedSources: [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer], | ||||
|                 allowFallback: false, | ||||
|                 shouldBatchBridgeOrders: false, | ||||
|             }; | ||||
| @@ -363,9 +363,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const numSamples = _.random(1, NUM_SAMPLES); | ||||
|                 let actualNumSamples = 0; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                     getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         actualNumSamples = amounts.length; | ||||
|                         return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                         return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -378,9 +378,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('polls all DEXes if `excludedSources` is empty', async () => { | ||||
|                 let sourcesPolled: ERC20BridgeSource[] = []; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sources.slice(); | ||||
|                         return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sourcesPolled.concat(sources.slice()); | ||||
|                         return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -396,7 +396,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     DEFAULT_RATES, | ||||
|                 ); | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: fn, | ||||
|                     getSellQuotesAsync: fn, | ||||
|                 }); | ||||
|                 const registryAddress = randomAddress(); | ||||
|                 const newMarketOperationUtils = new MarketOperationUtils( | ||||
| @@ -419,9 +419,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); | ||||
|                 let sourcesPolled: ERC20BridgeSource[] = []; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sources.slice(); | ||||
|                         return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sourcesPolled.concat(sources.slice()); | ||||
|                         return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -477,7 +477,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 expect(improvedOrders).to.not.be.length(0); | ||||
|                 for (const order of improvedOrders) { | ||||
|                     const expectedMakerAmount = order.fills[0].output; | ||||
|                     const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); | ||||
|                     const slippage = new BigNumber(1).minus(order.makerAssetAmount.div(expectedMakerAmount.plus(1))); | ||||
|                     assertRoughlyEquals(slippage, bridgeSlippage, 1); | ||||
|                 } | ||||
|             }); | ||||
| @@ -485,11 +485,11 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('can mix convex sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [0.5, 0.05, 0.05, 0.05]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; | ||||
|                 rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -499,7 +499,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                 ]; | ||||
| @@ -514,18 +514,20 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const nativeFeeRate = 0.06; | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91] | ||||
|                     [ERC20BridgeSource.UniswapV2]: [0.96, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], | ||||
|                 }; | ||||
|                 const feeSchedule = { | ||||
|                     [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) | ||||
|                         .times(nativeFeeRate) | ||||
|                         .dividedToIntegerBy(ETH_TO_MAKER_RATE), | ||||
|                     [ERC20BridgeSource.Native]: _.constant( | ||||
|                         FILL_AMOUNT.div(4) | ||||
|                             .times(nativeFeeRate) | ||||
|                             .dividedToIntegerBy(ETH_TO_MAKER_RATE), | ||||
|                     ), | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -535,7 +537,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                 ]; | ||||
| @@ -551,16 +553,18 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], | ||||
|                     // Effectively [0.8, ~0.5, ~0, ~0] | ||||
|                     [ERC20BridgeSource.UniswapV2]: [1, 0.7, 0.2, 0.2], | ||||
|                     [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], | ||||
|                 }; | ||||
|                 const feeSchedule = { | ||||
|                     [ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4) | ||||
|                         .times(uniswapFeeRate) | ||||
|                         .dividedToIntegerBy(ETH_TO_MAKER_RATE), | ||||
|                     [ERC20BridgeSource.Uniswap]: _.constant( | ||||
|                         FILL_AMOUNT.div(4) | ||||
|                             .times(uniswapFeeRate) | ||||
|                             .dividedToIntegerBy(ETH_TO_MAKER_RATE), | ||||
|                     ), | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -571,7 +575,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                 ]; | ||||
|                 expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); | ||||
|             }); | ||||
| @@ -580,12 +584,12 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use | ||||
|                     [ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave | ||||
|                     [ERC20BridgeSource.UniswapV2]: [0.96, 0.2, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Uniswap]: [0.96, 0.2, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1], | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -595,7 +599,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                 ]; | ||||
|                 expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); | ||||
| @@ -604,11 +608,11 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('fallback orders use different sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [0.6, 0.05, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -620,7 +624,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                 ]; | ||||
|                 const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber]; | ||||
|                 expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); | ||||
| @@ -630,11 +634,11 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('does not create a fallback if below maxFallbackSlippage', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; | ||||
|                 rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -642,7 +646,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, | ||||
|                 ); | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.UniswapV2]; | ||||
|                 const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; | ||||
|                 const secondSources: ERC20BridgeSource[] = []; | ||||
|                 expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); | ||||
|                 expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); | ||||
| @@ -667,7 +671,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 ] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress); | ||||
|                 replaceSamplerOps({ | ||||
|                     getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], | ||||
|                     getSellQuotes: getSellQuotesFn, | ||||
|                     getSellQuotesAsync: getSellQuotesFn, | ||||
|                     getLiquidityProviderFromRegistry: getLiquidityProviderFn, | ||||
|                 }); | ||||
|  | ||||
| @@ -706,12 +710,12 @@ describe('MarketOperationUtils tests', () => { | ||||
|  | ||||
|             it('batches contiguous bridge sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [1, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.CurveUsdcDai] = [0.48, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                     getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( | ||||
|                     createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -721,7 +725,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         numSamples: 4, | ||||
|                         excludedSources: [ | ||||
|                             ERC20BridgeSource.Kyber, | ||||
|                             ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai), | ||||
|                             ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve), | ||||
|                         ], | ||||
|                         shouldBatchBridgeOrders: true, | ||||
|                     }, | ||||
| @@ -729,9 +733,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 expect(improvedOrders).to.be.length(3); | ||||
|                 const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); | ||||
|                 expect(orderFillSources).to.deep.eq([ | ||||
|                     [ERC20BridgeSource.UniswapV2], | ||||
|                     [ERC20BridgeSource.Uniswap], | ||||
|                     [ERC20BridgeSource.Native], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.CurveUsdcDai], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve], | ||||
|                 ]); | ||||
|             }); | ||||
|         }); | ||||
| @@ -748,10 +752,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 bridgeSlippage: 0, | ||||
|                 maxFallbackSlippage: 100, | ||||
|                 excludedSources: [ | ||||
|                     ...(Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[]), | ||||
|                     ERC20BridgeSource.Kyber, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.UniswapV2Eth, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Curve, | ||||
|                     ERC20BridgeSource.Balancer, | ||||
|                 ], | ||||
|                 allowFallback: false, | ||||
|                 shouldBatchBridgeOrders: false, | ||||
| @@ -765,9 +769,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const numSamples = _.random(1, 16); | ||||
|                 let actualNumSamples = 0; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                     getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         actualNumSamples = amounts.length; | ||||
|                         return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                         return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -780,9 +784,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('polls all DEXes if `excludedSources` is empty', async () => { | ||||
|                 let sourcesPolled: ERC20BridgeSource[] = []; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sources.slice(); | ||||
|                         return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sourcesPolled.concat(sources.slice()); | ||||
|                         return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -798,7 +802,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     DEFAULT_RATES, | ||||
|                 ); | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: fn, | ||||
|                     getBuyQuotesAsync: fn, | ||||
|                 }); | ||||
|                 const registryAddress = randomAddress(); | ||||
|                 const newMarketOperationUtils = new MarketOperationUtils( | ||||
| @@ -821,9 +825,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); | ||||
|                 let sourcesPolled: ERC20BridgeSource[] = []; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sources.slice(); | ||||
|                         return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { | ||||
|                         sourcesPolled = sourcesPolled.concat(sources.slice()); | ||||
|                         return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); | ||||
|                     }, | ||||
|                 }); | ||||
|                 await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { | ||||
| @@ -879,7 +883,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 expect(improvedOrders).to.not.be.length(0); | ||||
|                 for (const order of improvedOrders) { | ||||
|                     const expectedTakerAmount = order.fills[0].output; | ||||
|                     const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; | ||||
|                     const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).minus(1); | ||||
|                     assertRoughlyEquals(slippage, bridgeSlippage, 1); | ||||
|                 } | ||||
|             }); | ||||
| @@ -887,10 +891,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('can mix convex sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [0.5, 0.05, 0.05, 0.05]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -900,7 +904,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                 ]; | ||||
| @@ -915,18 +919,20 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const nativeFeeRate = 0.06; | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91] | ||||
|                     [ERC20BridgeSource.UniswapV2]: [0.96, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1], | ||||
|                     [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1], | ||||
|                 }; | ||||
|                 const feeSchedule = { | ||||
|                     [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) | ||||
|                         .times(nativeFeeRate) | ||||
|                         .dividedToIntegerBy(ETH_TO_TAKER_RATE), | ||||
|                     [ERC20BridgeSource.Native]: _.constant( | ||||
|                         FILL_AMOUNT.div(4) | ||||
|                             .times(nativeFeeRate) | ||||
|                             .dividedToIntegerBy(ETH_TO_TAKER_RATE), | ||||
|                     ), | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -935,7 +941,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 ); | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
| @@ -950,17 +956,19 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const rates: RatesBySource = { | ||||
|                     [ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1], | ||||
|                     // Effectively [0.8, ~0.5, ~0, ~0] | ||||
|                     [ERC20BridgeSource.UniswapV2]: [1, 0.7, 0.2, 0.2], | ||||
|                     [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], | ||||
|                     [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], | ||||
|                 }; | ||||
|                 const feeSchedule = { | ||||
|                     [ERC20BridgeSource.UniswapV2]: FILL_AMOUNT.div(4) | ||||
|                         .times(uniswapFeeRate) | ||||
|                         .dividedToIntegerBy(ETH_TO_TAKER_RATE), | ||||
|                     [ERC20BridgeSource.Uniswap]: _.constant( | ||||
|                         FILL_AMOUNT.div(4) | ||||
|                             .times(uniswapFeeRate) | ||||
|                             .dividedToIntegerBy(ETH_TO_TAKER_RATE), | ||||
|                     ), | ||||
|                 }; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -971,7 +979,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const expectedSources = [ | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Eth2Dai, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                 ]; | ||||
|                 expect(orderSources.sort()).to.deep.eq(expectedSources.sort()); | ||||
|             }); | ||||
| @@ -979,10 +987,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('fallback orders use different sources', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [0.6, 0.05, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -994,7 +1002,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.Native, | ||||
|                     ERC20BridgeSource.UniswapV2, | ||||
|                     ERC20BridgeSource.Uniswap, | ||||
|                 ]; | ||||
|                 const secondSources = [ERC20BridgeSource.Eth2Dai]; | ||||
|                 expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); | ||||
| @@ -1004,10 +1012,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|             it('does not create a fallback if below maxFallbackSlippage', async () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -1015,7 +1023,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                     { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, | ||||
|                 ); | ||||
|                 const orderSources = improvedOrders.map(o => o.fills[0].source); | ||||
|                 const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.UniswapV2]; | ||||
|                 const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; | ||||
|                 const secondSources: ERC20BridgeSource[] = []; | ||||
|                 expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); | ||||
|                 expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); | ||||
| @@ -1025,9 +1033,9 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const rates: RatesBySource = {}; | ||||
|                 rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.UniswapV2] = [0.48, 0.47, 0.01, 0.01]; | ||||
|                 rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01]; | ||||
|                 replaceSamplerOps({ | ||||
|                     getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                     getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), | ||||
|                 }); | ||||
|                 const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( | ||||
|                     createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), | ||||
| @@ -1042,7 +1050,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); | ||||
|                 expect(orderFillSources).to.deep.eq([ | ||||
|                     [ERC20BridgeSource.Native], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.UniswapV2], | ||||
|                     [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], | ||||
|                 ]); | ||||
|             }); | ||||
|         }); | ||||
| @@ -1078,7 +1086,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|         }; | ||||
|         const orders = [smallOrder, largeOrder]; | ||||
|         const feeSchedule = { | ||||
|             [ERC20BridgeSource.Native]: new BigNumber(2e5), | ||||
|             [ERC20BridgeSource.Native]: _.constant(2e5), | ||||
|         }; | ||||
|  | ||||
|         it('penalizes native fill based on target amount when target is smaller', () => { | ||||
|   | ||||
| @@ -22,7 +22,7 @@ describe('quote_simulation tests', async () => { | ||||
|     const TAKER_TOKEN = randomAddress(); | ||||
|     const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); | ||||
|     const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); | ||||
|     const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; | ||||
|     const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: _.constant(1) }; | ||||
|  | ||||
|     // Check if two numbers are within `maxError` error rate within each other (default 1 bps). | ||||
|     function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-12): void { | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| import { BalancerPool, BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils'; | ||||
|  | ||||
| export interface Handlers { | ||||
|     getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>; | ||||
|     _fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>; | ||||
| } | ||||
|  | ||||
| export class MockBalancerPoolsCache extends BalancerPoolsCache { | ||||
|     constructor(public handlers: Partial<Handlers>) { | ||||
|         super(); | ||||
|     } | ||||
|  | ||||
|     public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> { | ||||
|         return this.handlers.getPoolsForPairAsync | ||||
|             ? this.handlers.getPoolsForPairAsync(takerToken, makerToken) | ||||
|             : super.getPoolsForPairAsync(takerToken, makerToken); | ||||
|     } | ||||
|  | ||||
|     protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> { | ||||
|         return this.handlers._fetchPoolsForPairAsync | ||||
|             ? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken) | ||||
|             : super._fetchPoolsForPairAsync(takerToken, makerToken); | ||||
|     } | ||||
| } | ||||
| @@ -226,6 +226,9 @@ export class MockSamplerContract extends IERC20BridgeSamplerContract { | ||||
|     } | ||||
|  | ||||
|     private _callEncodedFunction(callData: string): string { | ||||
|         if (callData === '0x') { | ||||
|             return callData; | ||||
|         } | ||||
|         // tslint:disable-next-line: custom-no-magic-numbers | ||||
|         const selector = hexUtils.slice(callData, 0, 4); | ||||
|         for (const [name, handler] of Object.entries(this._handlers)) { | ||||
|   | ||||
| @@ -25,6 +25,10 @@ | ||||
|             { | ||||
|                 "note": "Update ganache snapshot Exchange Proxy addresses for MetaTransactions", | ||||
|                 "pr": 2610 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Add BalancerBridge addresses", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -33,6 +33,7 @@ | ||||
|         "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", | ||||
|         "dexForwarderBridge": "0xc47b7094f378e54347e281aab170e8cca69d880a", | ||||
|         "multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1", | ||||
|         "balancerBridge": "0xfe01821ca163844203220cd08e4f2b2fb43ae4e4", | ||||
|         "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", | ||||
|         "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", | ||||
|         "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", | ||||
| @@ -79,6 +80,7 @@ | ||||
|         "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", | ||||
|         "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", | ||||
|         "multiBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "balancerBridge": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", | ||||
|         "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", | ||||
|         "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", | ||||
|         "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", | ||||
| @@ -125,6 +127,7 @@ | ||||
|         "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", | ||||
|         "dexForwarderBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "multiBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "balancerBridge": "0x5d8c9ba74607d2cbc4176882a42d4ace891c1c00", | ||||
|         "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", | ||||
|         "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", | ||||
|         "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", | ||||
| @@ -171,6 +174,7 @@ | ||||
|         "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74", | ||||
|         "dexForwarderBridge": "0xf220eb0b29e18bbc8ebc964e915b7547c7b4de4f", | ||||
|         "multiBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "balancerBridge": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", | ||||
|         "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", | ||||
|         "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", | ||||
|         "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", | ||||
| @@ -217,6 +221,7 @@ | ||||
|         "maximumGasPrice": "0x0000000000000000000000000000000000000000", | ||||
|         "dexForwarderBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "multiBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "balancerBridge": "0x0000000000000000000000000000000000000000", | ||||
|         "exchangeProxyGovernor": "0x0000000000000000000000000000000000000000", | ||||
|         "exchangeProxy": "0x2ebb94cc79d7d0f1195300aaf191d118f53292a8", | ||||
|         "exchangeProxyAllowanceTarget": "0x3eab3df72fd584b50184ff7d988a0d8f9328c866", | ||||
|   | ||||
| @@ -34,6 +34,7 @@ export interface ContractAddresses { | ||||
|     maximumGasPrice: string; | ||||
|     dexForwarderBridge: string; | ||||
|     multiBridge: string; | ||||
|     balancerBridge: string; | ||||
|     exchangeProxyGovernor: string; | ||||
|     exchangeProxy: string; | ||||
|     exchangeProxyAllowanceTarget: string; | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -9,6 +9,10 @@ | ||||
|             { | ||||
|                 "note": "Add affiliate fee transformer migration and flash wallet address", | ||||
|                 "pr": 2622 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Add BalancerBridge to returned object in `migration.ts`", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -379,6 +379,7 @@ export async function runMigrationsAsync( | ||||
|         maximumGasPrice: NULL_ADDRESS, | ||||
|         dexForwarderBridge: NULL_ADDRESS, | ||||
|         multiBridge: NULL_ADDRESS, | ||||
|         balancerBridge: NULL_ADDRESS, | ||||
|         exchangeProxyGovernor: NULL_ADDRESS, | ||||
|         exchangeProxy: exchangeProxy.address, | ||||
|         exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, | ||||
|   | ||||
| @@ -9,6 +9,10 @@ | ||||
|             { | ||||
|                 "note": "Add `Set` to `EXTERNAL_TYPE_MAP`.", | ||||
|                 "pr": 2350 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Add `TFillData` to `EXTERNAL_TYPE_MAP`", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -24,6 +24,8 @@ export const docGenConfigs: DocGenConfigs = { | ||||
|         // HACK: Asset-swapper specifies marketSell and marketBuy quotes with a descriminant MarketOperation Type to ignore the error, linking Buy and Sell to MarketOperation | ||||
|         Buy: true, | ||||
|         Sell: true, | ||||
|         // HACK: Asset-swapper specifies TFillData as any type that extends FillData | ||||
|         TFillData: true, | ||||
|         IterableIterator: true, | ||||
|         Set: true, | ||||
|     }, | ||||
|   | ||||
| @@ -1,4 +1,13 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "4.1.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Set `no-non-null-assertion` to false", | ||||
|                 "pr": 2613 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "version": "4.0.0", | ||||
|         "changes": [ | ||||
|   | ||||
| @@ -65,7 +65,7 @@ | ||||
|         "no-lodash-isnull": true, | ||||
|         "no-lodash-isundefined": true, | ||||
|         "no-misused-new": true, | ||||
|         "no-non-null-assertion": true, | ||||
|         "no-non-null-assertion": false, | ||||
|         "no-parameter-reassignment": true, | ||||
|         "no-redundant-jsdoc": true, | ||||
|         "no-return-await": true, | ||||
|   | ||||
| @@ -34,4 +34,9 @@ if (isNode) { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // HACK: CLobber config and set to prevent imported packages from poisoning | ||||
| // global BigNumber config | ||||
| (orig => (BigNumber.config = (..._args: any[]) => orig({})))(BigNumber.config); | ||||
| BigNumber.set = BigNumber.config; | ||||
|  | ||||
| export { BigNumber }; | ||||
|   | ||||
							
								
								
									
										47
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1047,6 +1047,16 @@ | ||||
|     lodash "^4.17.13" | ||||
|     to-fast-properties "^2.0.0" | ||||
|  | ||||
| "@balancer-labs/sor@^0.3.0": | ||||
|   version "0.3.0" | ||||
|   resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-0.3.0.tgz#c221225d9a3d1791ebfc3c566f7a76843bca98fa" | ||||
|   integrity sha512-QTVkeDmcGCaEgBhcVSu8c7cz6HA1ueWRbniuT+Yh0N/sqcZIcDMdoCFcpq66SD+hOxQ88RvzShmJ+P/3vKbXfg== | ||||
|   dependencies: | ||||
|     bignumber.js "^9.0.0" | ||||
|     ethers "^4.0.39" | ||||
|     isomorphic-fetch "^2.2.1" | ||||
|     typescript "^3.8.3" | ||||
|  | ||||
| "@cnakazawa/watch@^1.0.3": | ||||
|   version "1.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" | ||||
| @@ -3906,7 +3916,7 @@ big.js@^5.2.2: | ||||
|   version "5.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" | ||||
|  | ||||
| bignumber.js@*, bignumber.js@~9.0.0: | ||||
| bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@~9.0.0: | ||||
|   version "9.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" | ||||
|  | ||||
| @@ -6283,6 +6293,19 @@ elliptic@6.3.3: | ||||
|     hash.js "^1.0.0" | ||||
|     inherits "^2.0.1" | ||||
|  | ||||
| elliptic@6.5.2: | ||||
|   version "6.5.2" | ||||
|   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" | ||||
|   integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== | ||||
|   dependencies: | ||||
|     bn.js "^4.4.0" | ||||
|     brorand "^1.0.1" | ||||
|     hash.js "^1.0.0" | ||||
|     hmac-drbg "^1.0.0" | ||||
|     inherits "^2.0.1" | ||||
|     minimalistic-assert "^1.0.0" | ||||
|     minimalistic-crypto-utils "^1.0.0" | ||||
|  | ||||
| elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0: | ||||
|   version "6.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" | ||||
| @@ -7115,6 +7138,21 @@ ethers@4.0.0-beta.3: | ||||
|     uuid "2.0.1" | ||||
|     xmlhttprequest "1.8.0" | ||||
|  | ||||
| ethers@^4.0.39: | ||||
|   version "4.0.47" | ||||
|   resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.47.tgz#91b9cd80473b1136dd547095ff9171bd1fc68c85" | ||||
|   integrity sha512-hssRYhngV4hiDNeZmVU/k5/E8xmLG8UpcNUzg6mb7lqhgpFPH/t7nuv20RjRrEf0gblzvi2XwR5Te+V3ZFc9pQ== | ||||
|   dependencies: | ||||
|     aes-js "3.0.0" | ||||
|     bn.js "^4.4.0" | ||||
|     elliptic "6.5.2" | ||||
|     hash.js "1.1.3" | ||||
|     js-sha3 "0.5.7" | ||||
|     scrypt-js "2.0.4" | ||||
|     setimmediate "1.0.4" | ||||
|     uuid "2.0.1" | ||||
|     xmlhttprequest "1.8.0" | ||||
|  | ||||
| ethers@~4.0.4: | ||||
|   version "4.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65" | ||||
| @@ -9771,7 +9809,7 @@ isobject@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" | ||||
|  | ||||
| isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1: | ||||
| isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1: | ||||
|   version "2.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" | ||||
|   dependencies: | ||||
| @@ -16802,6 +16840,11 @@ typescript@3.5.x: | ||||
|   version "3.5.3" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" | ||||
|  | ||||
| typescript@^3.8.3: | ||||
|   version "3.9.6" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" | ||||
|   integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== | ||||
|  | ||||
| typewise-core@^1.2, typewise-core@^1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user