Feature/bunny hop (#2647)

* `@0x/contracts-erc20-bridge-sampler`: Add TwoHopSampler + refactor

* `@0x/asset-swapper`: Refactor + add two-hop skeleton

* Round out two-hop support in asset-swapper

* Add BalancerSampler, use it for two-hop quotes

* Fix bugs discovered from simbot

* rebases are hard

* Add intermediate token to MultiHop source breakdown

* Fix market buy bugs

* Use hybrid on-chain/off-chain sampling for Balancer

* Another day, another rebase

* Update changelogs

* Address PR feedback, CI fixes

* Address more PR feedback
This commit is contained in:
mzhu25 2020-08-26 15:20:09 -07:00 committed by GitHub
parent 4f78f55c2a
commit bab34c2d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2714 additions and 2078 deletions

View File

@ -11,6 +11,7 @@
}, },
"scripts": { "scripts": {
"build": "yarn pre_build && tsc -b", "build": "yarn pre_build && tsc -b",
"build:ts": "tsc -b",
"build:ci": "yarn build", "build:ci": "yarn build",
"pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy", "pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy",
"test": "yarn run_mocha", "test": "yarn run_mocha",

View File

@ -97,7 +97,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize( const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize(
this.actor.deployment.tokens.erc20, this.actor.deployment.tokens.erc20,
4, // tslint:disable-line:custom-no-magic-numbers 4, // tslint:disable-line:custom-no-magic-numbers
); )!;
// Maker and taker set balances/allowances to guarantee that the fill succeeds. // Maker and taker set balances/allowances to guarantee that the fill succeeds.
// Amounts are chosen to be within each actor's balance (divided by 8, in case // Amounts are chosen to be within each actor's balance (divided by 8, in case
@ -146,7 +146,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const [leftMakerToken, leftTakerToken, makerFeeToken, takerFeeToken] = Pseudorandom.sampleSize( const [leftMakerToken, leftTakerToken, makerFeeToken, takerFeeToken] = Pseudorandom.sampleSize(
this.actor.deployment.tokens.erc20, this.actor.deployment.tokens.erc20,
4, // tslint:disable-line:custom-no-magic-numbers 4, // tslint:disable-line:custom-no-magic-numbers
); )!;
const rightMakerToken = leftTakerToken; const rightMakerToken = leftTakerToken;
const rightTakerToken = leftMakerToken; const rightTakerToken = leftMakerToken;

View File

@ -144,7 +144,7 @@ function _getParameterNames(func: (...args: any[]) => any): string[] {
.replace(/[/][/].*$/gm, '') // strip single-line comments .replace(/[/][/].*$/gm, '') // strip single-line comments
.replace(/\s+/g, '') // strip white space .replace(/\s+/g, '') // strip white space
.replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
.split('){', 1)[0] .split(/\){|\)=>/, 1)[0]
.replace(/^[^(]*[(]/, '') // extract the parameters .replace(/^[^(]*[(]/, '') // extract the parameters
.replace(/=[^,]+/g, '') // strip any ES6 defaults .replace(/=[^,]+/g, '') // strip any ES6 defaults
.split(',') .split(',')

View File

@ -61,6 +61,18 @@
{ {
"note": "Added `Mooniswap`", "note": "Added `Mooniswap`",
"pr": 2675 "pr": 2675
},
{
"note": "Added two-hop support",
"pr": 2647
},
{
"note": "Move ERC20BridgeSampler interfaces into `interfaces` directory",
"pr": 2647
},
{
"note": "Use on-chain sampling (sometimes) for Balancer",
"pr": 2647
} }
] ]
}, },

View File

@ -0,0 +1,143 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./interfaces/IBalancer.sol";
contract BalancerSampler {
/// @dev Base gas limit for Balancer calls.
uint256 constant private BALANCER_CALL_GAS = 300e3; // 300k
struct BalancerState {
uint256 takerTokenBalance;
uint256 makerTokenBalance;
uint256 takerTokenWeight;
uint256 makerTokenWeight;
uint256 swapFee;
}
/// @dev Sample sell quotes from Balancer.
/// @param poolAddress Address of the Balancer pool to query.
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromBalancer(
address poolAddress,
address takerToken,
address makerToken,
uint256[] memory takerTokenAmounts
)
public
view
returns (uint256[] memory makerTokenAmounts)
{
IBalancer pool = IBalancer(poolAddress);
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) {
return makerTokenAmounts;
}
BalancerState memory poolState;
poolState.takerTokenBalance = pool.getBalance(takerToken);
poolState.makerTokenBalance = pool.getBalance(makerToken);
poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken);
poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken);
poolState.swapFee = pool.getSwapFee();
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
poolAddress.staticcall.gas(BALANCER_CALL_GAS)(
abi.encodeWithSelector(
pool.calcOutGivenIn.selector,
poolState.takerTokenBalance,
poolState.takerTokenWeight,
poolState.makerTokenBalance,
poolState.makerTokenWeight,
takerTokenAmounts[i],
poolState.swapFee
));
uint256 buyAmount = 0;
if (didSucceed) {
buyAmount = abi.decode(resultData, (uint256));
} else {
break;
}
makerTokenAmounts[i] = buyAmount;
}
}
/// @dev Sample buy quotes from Balancer.
/// @param poolAddress Address of the Balancer pool to query.
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param makerTokenAmounts Maker token buy amount for each sample.
/// @return takerTokenAmounts Taker amounts sold at each maker token
/// amount.
function sampleBuysFromBalancer(
address poolAddress,
address takerToken,
address makerToken,
uint256[] memory makerTokenAmounts
)
public
view
returns (uint256[] memory takerTokenAmounts)
{
IBalancer pool = IBalancer(poolAddress);
uint256 numSamples = makerTokenAmounts.length;
takerTokenAmounts = new uint256[](numSamples);
if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) {
return takerTokenAmounts;
}
BalancerState memory poolState;
poolState.takerTokenBalance = pool.getBalance(takerToken);
poolState.makerTokenBalance = pool.getBalance(makerToken);
poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken);
poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken);
poolState.swapFee = pool.getSwapFee();
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
poolAddress.staticcall.gas(BALANCER_CALL_GAS)(
abi.encodeWithSelector(
pool.calcInGivenOut.selector,
poolState.takerTokenBalance,
poolState.takerTokenWeight,
poolState.makerTokenBalance,
poolState.makerTokenWeight,
makerTokenAmounts[i],
poolState.swapFee
));
uint256 sellAmount = 0;
if (didSucceed) {
sellAmount = abi.decode(resultData, (uint256));
} else {
break;
}
takerTokenAmounts[i] = sellAmount;
}
}
}

View File

@ -19,7 +19,7 @@
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "./ICurve.sol"; import "./interfaces/ICurve.sol";
import "./ApproximateBuys.sol"; import "./ApproximateBuys.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";

View File

@ -19,6 +19,7 @@
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "./BalancerSampler.sol";
import "./CurveSampler.sol"; import "./CurveSampler.sol";
import "./Eth2DaiSampler.sol"; import "./Eth2DaiSampler.sol";
import "./KyberSampler.sol"; import "./KyberSampler.sol";
@ -29,9 +30,11 @@ import "./MooniswapSampler.sol";
import "./NativeOrderSampler.sol"; import "./NativeOrderSampler.sol";
import "./UniswapSampler.sol"; import "./UniswapSampler.sol";
import "./UniswapV2Sampler.sol"; import "./UniswapV2Sampler.sol";
import "./TwoHopSampler.sol";
contract ERC20BridgeSampler is contract ERC20BridgeSampler is
BalancerSampler,
CurveSampler, CurveSampler,
Eth2DaiSampler, Eth2DaiSampler,
KyberSampler, KyberSampler,
@ -40,6 +43,7 @@ contract ERC20BridgeSampler is
MooniswapSampler, MooniswapSampler,
MultiBridgeSampler, MultiBridgeSampler,
NativeOrderSampler, NativeOrderSampler,
TwoHopSampler,
UniswapSampler, UniswapSampler,
UniswapV2Sampler UniswapV2Sampler
{ {

View File

@ -20,7 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IEth2Dai.sol"; import "./interfaces/IEth2Dai.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";

View File

@ -20,10 +20,10 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IKyberNetwork.sol"; import "./interfaces/IKyberNetwork.sol";
import "./IKyberNetworkProxy.sol"; import "./interfaces/IKyberNetworkProxy.sol";
import "./IKyberStorage.sol"; import "./interfaces/IKyberStorage.sol";
import "./IKyberHintHandler.sol"; import "./interfaces/IKyberHintHandler.sol";
import "./ApproximateBuys.sol"; import "./ApproximateBuys.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";

View File

@ -20,8 +20,8 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./ILiquidityProvider.sol"; import "./interfaces/ILiquidityProvider.sol";
import "./ILiquidityProviderRegistry.sol"; import "./interfaces/ILiquidityProviderRegistry.sol";
import "./ApproximateBuys.sol"; import "./ApproximateBuys.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";
@ -48,21 +48,21 @@ contract LiquidityProviderSampler is
) )
public public
view view
returns (uint256[] memory makerTokenAmounts) returns (uint256[] memory makerTokenAmounts, address providerAddress)
{ {
// Initialize array of maker token amounts. // Initialize array of maker token amounts.
uint256 numSamples = takerTokenAmounts.length; uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples); makerTokenAmounts = new uint256[](numSamples);
// Query registry for provider address. // Query registry for provider address.
address providerAddress = getLiquidityProviderFromRegistry( providerAddress = _getLiquidityProviderFromRegistry(
registryAddress, registryAddress,
takerToken, takerToken,
makerToken makerToken
); );
// If provider doesn't exist, return all zeros. // If provider doesn't exist, return all zeros.
if (providerAddress == address(0)) { if (providerAddress == address(0)) {
return makerTokenAmounts; return (makerTokenAmounts, providerAddress);
} }
for (uint256 i = 0; i < numSamples; i++) { for (uint256 i = 0; i < numSamples; i++) {
@ -101,9 +101,14 @@ contract LiquidityProviderSampler is
) )
public public
view view
returns (uint256[] memory takerTokenAmounts) returns (uint256[] memory takerTokenAmounts, address providerAddress)
{ {
return _sampleApproximateBuys( providerAddress = _getLiquidityProviderFromRegistry(
registryAddress,
takerToken,
makerToken
);
takerTokenAmounts = _sampleApproximateBuys(
ApproximateBuyQuoteOpts({ ApproximateBuyQuoteOpts({
makerTokenData: abi.encode(makerToken, registryAddress), makerTokenData: abi.encode(makerToken, registryAddress),
takerTokenData: abi.encode(takerToken, registryAddress), takerTokenData: abi.encode(takerToken, registryAddress),
@ -119,12 +124,12 @@ contract LiquidityProviderSampler is
/// @param takerToken Taker asset managed by liquidity provider. /// @param takerToken Taker asset managed by liquidity provider.
/// @param makerToken Maker asset managed by liquidity provider. /// @param makerToken Maker asset managed by liquidity provider.
/// @return providerAddress Address of the liquidity provider. /// @return providerAddress Address of the liquidity provider.
function getLiquidityProviderFromRegistry( function _getLiquidityProviderFromRegistry(
address registryAddress, address registryAddress,
address takerToken, address takerToken,
address makerToken address makerToken
) )
public private
view view
returns (address providerAddress) returns (address providerAddress)
{ {
@ -167,6 +172,7 @@ contract LiquidityProviderSampler is
return 0; return 0;
} }
// solhint-disable-next-line indent // solhint-disable-next-line indent
return abi.decode(resultData, (uint256[]))[0]; (uint256[] memory amounts, ) = abi.decode(resultData, (uint256[], address));
return amounts[0];
} }
} }

View File

@ -21,7 +21,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./IMStable.sol"; import "./interfaces/IMStable.sol";
import "./ApproximateBuys.sol"; import "./ApproximateBuys.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";

View File

@ -19,7 +19,7 @@
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "./IMultiBridge.sol"; import "./interfaces/IMultiBridge.sol";
contract MultiBridgeSampler { contract MultiBridgeSampler {

View File

@ -0,0 +1,125 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
contract TwoHopSampler {
using LibBytes for bytes;
struct HopInfo {
uint256 sourceIndex;
bytes returnData;
}
function sampleTwoHopSell(
bytes[] memory firstHopCalls,
bytes[] memory secondHopCalls,
uint256 sellAmount
)
public
view
returns (
HopInfo memory firstHop,
HopInfo memory secondHop,
uint256 buyAmount
)
{
uint256 intermediateAssetAmount = 0;
for (uint256 i = 0; i != firstHopCalls.length; ++i) {
firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, sellAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (amount > intermediateAssetAmount) {
intermediateAssetAmount = amount;
firstHop.sourceIndex = i;
firstHop.returnData = returnData;
}
}
}
if (intermediateAssetAmount == 0) {
return (firstHop, secondHop, buyAmount);
}
for (uint256 j = 0; j != secondHopCalls.length; ++j) {
secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, intermediateAssetAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (amount > buyAmount) {
buyAmount = amount;
secondHop.sourceIndex = j;
secondHop.returnData = returnData;
}
}
}
}
function sampleTwoHopBuy(
bytes[] memory firstHopCalls,
bytes[] memory secondHopCalls,
uint256 buyAmount
)
public
view
returns (
HopInfo memory firstHop,
HopInfo memory secondHop,
uint256 sellAmount
)
{
sellAmount = uint256(-1);
uint256 intermediateAssetAmount = uint256(-1);
for (uint256 j = 0; j != secondHopCalls.length; ++j) {
secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, buyAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (
amount > 0 &&
amount < intermediateAssetAmount
) {
intermediateAssetAmount = amount;
secondHop.sourceIndex = j;
secondHop.returnData = returnData;
}
}
}
if (intermediateAssetAmount == uint256(-1)) {
return (firstHop, secondHop, sellAmount);
}
for (uint256 i = 0; i != firstHopCalls.length; ++i) {
firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, intermediateAssetAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (
amount > 0 &&
amount < sellAmount
) {
sellAmount = amount;
firstHop.sourceIndex = i;
firstHop.returnData = returnData;
}
}
}
}
}

View File

@ -25,7 +25,7 @@ import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./IUniswapExchangeQuotes.sol"; import "./interfaces/IUniswapExchangeQuotes.sol";
import "./SamplerUtils.sol"; import "./SamplerUtils.sol";

View File

@ -20,7 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IUniswapV2Router01.sol"; import "./interfaces/IUniswapV2Router01.sol";
contract UniswapV2Sampler is contract UniswapV2Sampler is

View File

@ -0,0 +1,43 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IBalancer {
function isBound(address t) external view returns (bool);
function getDenormalizedWeight(address token) external view returns (uint256);
function getBalance(address token) external view returns (uint256);
function getSwapFee() external view returns (uint256);
function calcOutGivenIn(
uint256 tokenBalanceIn,
uint256 tokenWeightIn,
uint256 tokenBalanceOut,
uint256 tokenWeightOut,
uint256 tokenAmountIn,
uint256 swapFee
) external pure returns (uint256 tokenAmountOut);
function calcInGivenOut(
uint256 tokenBalanceIn,
uint256 tokenWeightIn,
uint256 tokenBalanceOut,
uint256 tokenWeightOut,
uint256 tokenAmountOut,
uint256 swapFee
) external pure returns (uint256 tokenAmountIn);
}

View File

@ -22,9 +22,9 @@ import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFacto
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "../src/ERC20BridgeSampler.sol"; import "../src/ERC20BridgeSampler.sol";
import "../src/IEth2Dai.sol"; import "../src/interfaces/IEth2Dai.sol";
import "../src/IKyberNetworkProxy.sol"; import "../src/interfaces/IKyberNetworkProxy.sol";
import "../src/IUniswapV2Router01.sol"; import "../src/interfaces/IUniswapV2Router01.sol";
library LibDeterministicQuotes { library LibDeterministicQuotes {

View File

@ -9,6 +9,7 @@
"types": "lib/src/index.d.ts", "types": "lib/src/index.d.ts",
"scripts": { "scripts": {
"build": "yarn pre_build && tsc -b", "build": "yarn pre_build && tsc -b",
"build:ts": "tsc -b",
"watch": "tsc -w -p tsconfig.json", "watch": "tsc -w -p tsconfig.json",
"watch:contracts": "sol-compiler -w", "watch:contracts": "sol-compiler -w",
"build:ci": "yarn build", "build:ci": "yarn build",
@ -37,7 +38,7 @@
"config": { "config": {
"publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(ApproximateBuys|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|UniswapSampler|UniswapV2Sampler).json", "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json",
"postpublish": { "postpublish": {
"assets": [] "assets": []
} }
@ -60,6 +61,7 @@
"@0x/base-contract": "^6.2.3", "@0x/base-contract": "^6.2.3",
"@0x/contract-addresses": "^4.11.0", "@0x/contract-addresses": "^4.11.0",
"@0x/contract-wrappers": "^13.8.0", "@0x/contract-wrappers": "^13.8.0",
"@0x/contracts-erc20-bridge-sampler": "^1.7.0",
"@0x/json-schemas": "^5.1.0", "@0x/json-schemas": "^5.1.0",
"@0x/order-utils": "^10.3.0", "@0x/order-utils": "^10.3.0",
"@0x/orderbook": "^2.2.7", "@0x/orderbook": "^2.2.7",

View File

@ -39,10 +39,8 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
...{ chainId: MAINNET_CHAIN_ID,
chainId: MAINNET_CHAIN_ID, orderRefreshIntervalMs: 10000, // 10 seconds
orderRefreshIntervalMs: 10000, // 10 seconds
},
...DEFAULT_ORDER_PRUNER_OPTS, ...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 250e6, samplerGasLimit: 250e6,
ethGasStationUrl: ETH_GAS_STATION_API_URL, ethGasStationUrl: ETH_GAS_STATION_API_URL,

View File

@ -1,3 +1,9 @@
export {
AwaitTransactionSuccessOpts,
ContractFunctionObj,
ContractTxFunctionObj,
SendTransactionOpts,
} from '@0x/base-contract';
export { ContractAddresses } from '@0x/contract-addresses'; export { ContractAddresses } from '@0x/contract-addresses';
export { WSOpts } from '@0x/mesh-rpc-client'; export { WSOpts } from '@0x/mesh-rpc-client';
export { export {
@ -28,6 +34,7 @@ export {
AbiDefinition, AbiDefinition,
BlockParam, BlockParam,
BlockParamLiteral, BlockParamLiteral,
CallData,
CompilerOpts, CompilerOpts,
CompilerSettings, CompilerSettings,
CompilerSettingsMetadata, CompilerSettingsMetadata,
@ -66,6 +73,8 @@ export {
StateMutability, StateMutability,
SupportedProvider, SupportedProvider,
TupleDataItem, TupleDataItem,
TxData,
TxDataPayable,
Web3JsProvider, Web3JsProvider,
Web3JsV1Provider, Web3JsV1Provider,
Web3JsV2Provider, Web3JsV2Provider,
@ -107,6 +116,11 @@ export {
SwapQuoterRfqtOpts, SwapQuoterRfqtOpts,
} from './types'; } from './types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export {
Parameters,
SamplerContractCall,
SamplerContractOperation,
} from './utils/market_operation_utils/sampler_contract_operation';
export { export {
BancorFillData, BancorFillData,
BalancerFillData, BalancerFillData,
@ -114,22 +128,30 @@ export {
CurveFillData, CurveFillData,
CurveFunctionSelectors, CurveFunctionSelectors,
CurveInfo, CurveInfo,
DexSample,
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule, FeeSchedule,
Fill,
FillData, FillData,
FillFlags,
GetMarketOrdersRfqtOpts, GetMarketOrdersRfqtOpts,
LiquidityProviderFillData, LiquidityProviderFillData,
MarketDepth, MarketDepth,
MarketDepthSide, MarketDepthSide,
MultiBridgeFillData, MultiBridgeFillData,
MultiHopFillData,
NativeCollapsedFill, NativeCollapsedFill,
NativeFillData, NativeFillData,
OptimizedMarketOrder, OptimizedMarketOrder,
SourceInfo,
SourceQuoteOperation,
TokenAdjacencyGraph,
UniswapV2FillData, UniswapV2FillData,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { export {
BridgeReportSource, BridgeReportSource,
MultiHopReportSource,
NativeOrderbookReportSource, NativeOrderbookReportSource,
NativeRFQTReportSource, NativeRFQTReportSource,
QuoteReport, QuoteReport,
@ -140,3 +162,4 @@ export { rfqtMocker } from './utils/rfqt_mocker';
export { ERC20BridgeSamplerContract } from './wrappers'; export { ERC20BridgeSamplerContract } from './wrappers';
import { ERC20BridgeSource } from './utils/market_operation_utils/types'; import { ERC20BridgeSource } from './utils/market_operation_utils/types';
export type Native = ERC20BridgeSource.Native; export type Native = ERC20BridgeSource.Native;
export type MultiHop = ERC20BridgeSource.MultiHop;

View File

@ -1,16 +1,13 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { ITransformERC20Contract } from '@0x/contract-wrappers'; import { ITransformERC20Contract } from '@0x/contract-wrappers';
import { import {
assetDataUtils,
encodeAffiliateFeeTransformerData, encodeAffiliateFeeTransformerData,
encodeFillQuoteTransformerData, encodeFillQuoteTransformerData,
encodePayTakerTransformerData, encodePayTakerTransformerData,
encodeWethTransformerData, encodeWethTransformerData,
ERC20AssetData,
ETH_TOKEN_ADDRESS, ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide, FillQuoteTransformerSide,
} from '@0x/order-utils'; } from '@0x/order-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils'; import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as ethjs from 'ethereumjs-util'; import * as ethjs from 'ethereumjs-util';
@ -30,6 +27,7 @@ import {
SwapQuoteGetOutputOpts, SwapQuoteGetOutputOpts,
} from '../types'; } from '../types';
import { assert } from '../utils/assert'; import { assert } from '../utils/assert';
import { getTokenFromAssetData } from '../utils/utils';
// tslint:disable-next-line:custom-no-magic-numbers // tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
@ -108,19 +106,48 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
}); });
} }
const intermediateToken = quote.isTwoHop ? getTokenFromAssetData(quote.orders[0].makerAssetData) : NULL_ADDRESS;
// This transformer will fill the quote. // This transformer will fill the quote.
transforms.push({ if (quote.isTwoHop) {
deploymentNonce: this.transformerNonces.fillQuoteTransformer, const [firstHopOrder, secondHopOrder] = quote.orders;
data: encodeFillQuoteTransformerData({ transforms.push({
sellToken, deploymentNonce: this.transformerNonces.fillQuoteTransformer,
buyToken, data: encodeFillQuoteTransformerData({
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, sellToken,
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount, buyToken: intermediateToken,
maxOrderFillAmounts: [], side: FillQuoteTransformerSide.Sell,
orders: quote.orders, fillAmount: firstHopOrder.takerAssetAmount,
signatures: quote.orders.map(o => o.signature), maxOrderFillAmounts: [],
}), orders: [firstHopOrder],
}); signatures: [firstHopOrder.signature],
}),
});
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken: intermediateToken,
buyToken,
side: FillQuoteTransformerSide.Sell,
fillAmount: MAX_UINT256,
maxOrderFillAmounts: [],
orders: [secondHopOrder],
signatures: [secondHopOrder.signature],
}),
});
} else {
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken,
buyToken,
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
maxOrderFillAmounts: [],
orders: quote.orders,
signatures: quote.orders.map(o => o.signature),
}),
});
}
if (isToETH) { if (isToETH) {
// Create a WETH unwrapper if going to ETH. // Create a WETH unwrapper if going to ETH.
@ -159,7 +186,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
transforms.push({ transforms.push({
deploymentNonce: this.transformerNonces.payTakerTransformer, deploymentNonce: this.transformerNonces.payTakerTransformer,
data: encodePayTakerTransformerData({ data: encodePayTakerTransformerData({
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS], tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS].concat(quote.isTwoHop ? intermediateToken : []),
amounts: [], amounts: [],
}), }),
}); });
@ -201,15 +228,6 @@ function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy; return quote.type === MarketOperation.Buy;
} }
function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}
/** /**
* Find the nonce for a transformer given its deployer. * Find the nonce for a transformer given its deployer.
* If `deployer` is the null address, zero will always be returned. * If `deployer` is the null address, zero will always be returned.

View File

@ -2,7 +2,7 @@ import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/cont
import { DevUtilsContract } from '@0x/contract-wrappers'; import { DevUtilsContract } from '@0x/contract-wrappers';
import { schemas } from '@0x/json-schemas'; import { schemas } from '@0x/json-schemas';
import { assetDataUtils, SignedOrder } from '@0x/order-utils'; import { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { APIOrder, MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook'; import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { BigNumber, providerUtils } from '@0x/utils'; import { BigNumber, providerUtils } from '@0x/utils';
import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-types'; import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -165,6 +165,7 @@ export class SwapQuoter {
samplerGasLimit, samplerGasLimit,
liquidityProviderRegistryAddress, liquidityProviderRegistryAddress,
rfqt, rfqt,
tokenAdjacencyGraph,
} = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
const provider = providerUtils.standardizeOrThrow(supportedProvider); const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderbook('orderbook', orderbook); assert.isValidOrderbook('orderbook', orderbook);
@ -210,6 +211,7 @@ export class SwapQuoter {
exchangeAddress: this._contractAddresses.exchange, exchangeAddress: this._contractAddresses.exchange,
}, },
liquidityProviderRegistryAddress, liquidityProviderRegistryAddress,
tokenAdjacencyGraph,
); );
this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils); this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils);
} }
@ -422,7 +424,7 @@ export class SwapQuoter {
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
let [sellOrders, buyOrders] = let [sellOrders, buyOrders] =
options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native) options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native)
? await Promise.resolve([[] as APIOrder[], [] as APIOrder[]]) ? [[], []]
: await Promise.all([ : await Promise.all([
this.orderbook.getOrdersAsync(makerAssetData, takerAssetData), this.orderbook.getOrdersAsync(makerAssetData, takerAssetData),
this.orderbook.getOrdersAsync(takerAssetData, makerAssetData), this.orderbook.getOrdersAsync(takerAssetData, makerAssetData),

View File

@ -2,7 +2,12 @@ import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-w
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types'; import {
ERC20BridgeSource,
GetMarketOrdersOpts,
OptimizedMarketOrder,
TokenAdjacencyGraph,
} from './utils/market_operation_utils/types';
import { QuoteReport } from './utils/quote_report_generator'; import { QuoteReport } from './utils/quote_report_generator';
import { LogFunction } from './utils/quote_requestor'; import { LogFunction } from './utils/quote_requestor';
@ -141,8 +146,6 @@ export interface ExchangeProxyContractOpts {
affiliateFee: AffiliateFee; affiliateFee: AffiliateFee;
} }
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
export interface GetExtensionContractTypeOpts { export interface GetExtensionContractTypeOpts {
takerAddress?: string; takerAddress?: string;
ethAmount?: BigNumber; ethAmount?: BigNumber;
@ -165,6 +168,7 @@ export interface SwapQuoteBase {
worstCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown; sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport; quoteReport?: QuoteReport;
isTwoHop: boolean;
} }
/** /**
@ -185,6 +189,8 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
type: MarketOperation.Buy; type: MarketOperation.Buy;
} }
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
/** /**
* feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets. * feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerAssetAmount: The amount of takerAsset swapped for desired makerAsset. * takerAssetAmount: The amount of takerAsset swapped for desired makerAsset.
@ -205,9 +211,15 @@ export interface SwapQuoteInfo {
/** /**
* percentage breakdown of each liquidity source used in quote * percentage breakdown of each liquidity source used in quote
*/ */
export interface SwapQuoteOrdersBreakdown { export type SwapQuoteOrdersBreakdown = Partial<
[source: string]: BigNumber; { [key in Exclude<ERC20BridgeSource, typeof ERC20BridgeSource.MultiHop>]: BigNumber } & {
} [ERC20BridgeSource.MultiHop]: {
proportion: BigNumber;
intermediateToken: string;
hops: ERC20BridgeSource[];
};
}
>;
/** /**
* nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity. * nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity.
@ -272,6 +284,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
ethGasStationUrl?: string; ethGasStationUrl?: string;
rfqt?: SwapQuoterRfqtOpts; rfqt?: SwapQuoterRfqtOpts;
samplerOverrides?: SamplerOverrides; samplerOverrides?: SamplerOverrides;
tokenAdjacencyGraph?: TokenAdjacencyGraph;
} }
/** /**

View File

@ -19,12 +19,21 @@ export const assert = {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData); sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData); sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema); sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
assert.isValidSwapQuoteOrders( if (swapQuote.isTwoHop) {
`${variableName}.orders`, assert.isValidTwoHopSwapQuoteOrders(
swapQuote.orders, `${variableName}.orders`,
swapQuote.makerAssetData, swapQuote.orders,
swapQuote.takerAssetData, swapQuote.makerAssetData,
); swapQuote.takerAssetData,
);
} else {
assert.isValidSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
swapQuote.takerAssetData,
);
}
assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo); assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo);
assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo); assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo);
if (swapQuote.type === MarketOperation.Buy) { if (swapQuote.type === MarketOperation.Buy) {
@ -54,6 +63,28 @@ export const assert = {
); );
}); });
}, },
isValidTwoHopSwapQuoteOrders(
variableName: string,
orders: SignedOrder[],
makerAssetData: string,
takerAssetData: string,
): void {
assert.assert(orders.length === 2, `Expected ${variableName}.length to be 2 for a two-hop quote`);
assert.assert(
isAssetDataEquivalent(takerAssetData, orders[0].takerAssetData),
`Expected ${variableName}[0].takerAssetData to be ${takerAssetData} but found ${orders[0].takerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(makerAssetData, orders[1].makerAssetData),
`Expected ${variableName}[1].makerAssetData to be ${makerAssetData} but found ${orders[1].makerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(orders[0].makerAssetData, orders[1].takerAssetData),
`Expected ${variableName}[0].makerAssetData (${
orders[0].makerAssetData
}) to equal ${variableName}[1].takerAssetData (${orders[1].takerAssetData})`,
);
},
isValidOrdersForSwapQuoter<T extends Order>(variableName: string, orders: T[]): void { isValidOrdersForSwapQuoter<T extends Order>(variableName: string, orders: T[]): void {
_.every(orders, (order: T, index: number) => { _.every(orders, (order: T, index: number) => {
assert.assert( assert.assert(

View File

@ -2,6 +2,10 @@ import { BigNumber } from '@0x/utils';
import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Decimal } from 'decimal.js'; import { Decimal } from 'decimal.js';
import { ERC20BridgeSource } from './types';
// tslint:disable:boolean-naming
export interface BalancerPool { export interface BalancerPool {
id: string; id: string;
balanceIn: BigNumber; balanceIn: BigNumber;
@ -21,15 +25,15 @@ interface CacheValue {
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000; const FIVE_SECONDS_MS = 5 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 1000; const DEFAULT_TIMEOUT_MS = 1000;
const MAX_POOLS_FETCHED = 2; const MAX_POOLS_FETCHED = 3;
const Decimal20 = Decimal.clone({ precision: 20 }); const Decimal20 = Decimal.clone({ precision: 20 });
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
export class BalancerPoolsCache { export class BalancerPoolsCache {
constructor( constructor(
private readonly _cache: { [key: string]: CacheValue } = {}, private readonly _cache: { [key: string]: CacheValue } = {},
public cacheExpiryMs: number = FIVE_SECONDS_MS,
private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED, private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED,
) {} ) {}
@ -42,10 +46,52 @@ export class BalancerPoolsCache {
return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]);
} }
protected async _getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> { public getCachedPoolAddressesForPair(
takerToken: string,
makerToken: string,
cacheExpiryMs?: number,
): string[] | undefined {
const key = JSON.stringify([takerToken, makerToken]); const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key]; const value = this._cache[key];
const minTimestamp = Date.now() - this.cacheExpiryMs; if (cacheExpiryMs === undefined) {
return value === undefined ? [] : value.pools.map(pool => pool.id);
}
const minTimestamp = Date.now() - cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) {
return undefined;
} else {
return value.pools.map(pool => pool.id);
}
}
public howToSampleBalancer(
takerToken: string,
makerToken: string,
excludedSources: ERC20BridgeSource[],
): { onChain: boolean; offChain: boolean } {
// If Balancer is excluded as a source, do not sample.
if (excludedSources.includes(ERC20BridgeSource.Balancer)) {
return { onChain: false, offChain: false };
}
const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS);
// Sample Balancer on-chain (i.e. via the ERC20BridgeSampler contract) if:
// - Cached values are not stale
// - There is at least one Balancer pool for this pair
const onChain = cachedBalancerPools !== undefined && cachedBalancerPools.length > 0;
// Sample Balancer off-chain (i.e. via GraphQL query + `computeBalancerBuy/SellQuote`)
// if cached values are stale
const offChain = cachedBalancerPools === undefined;
return { onChain, offChain };
}
protected async _getPoolsForPairAsync(
takerToken: string,
makerToken: string,
cacheExpiryMs: number = FIVE_SECONDS_MS,
): Promise<BalancerPool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) { if (value === undefined || value.timestamp < minTimestamp) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const timestamp = Date.now(); const timestamp = Date.now();

View File

@ -31,11 +31,7 @@ export class BancorService {
return this._sdk; return this._sdk;
} }
public async getQuoteAsync( public async getQuoteAsync(fromToken: string, toToken: string, amount: BigNumber): Promise<Quote<BancorFillData>> {
fromToken: string,
toToken: string,
amount: BigNumber = new BigNumber(1),
): Promise<Quote<BancorFillData>> {
const sdk = await this.getSDKAsync(); const sdk = await this.getSDKAsync();
const blockchain = sdk._core.blockchains[BlockchainType.Ethereum] as Ethereum; const blockchain = sdk._core.blockchains[BlockchainType.Ethereum] as Ethereum;
const sourceDecimals = await getDecimals(blockchain, fromToken); const sourceDecimals = await getDecimals(blockchain, fromToken);

View File

@ -138,6 +138,7 @@ export const ONE_ETHER = new BigNumber(1e18);
export const NEGATIVE_INF = new BigNumber('-Infinity'); export const NEGATIVE_INF = new BigNumber('-Infinity');
export const POSITIVE_INF = new BigNumber('Infinity'); export const POSITIVE_INF = new BigNumber('Infinity');
export const ZERO_AMOUNT = new BigNumber(0); export const ZERO_AMOUNT = new BigNumber(0);
export const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
export const ONE_HOUR_IN_SECONDS = 60 * 60; export const ONE_HOUR_IN_SECONDS = 60 * 60;
export const ONE_SECOND_MS = 1000; export const ONE_SECOND_MS = 1000;
export const NULL_BYTES = '0x'; export const NULL_BYTES = '0x';

View File

@ -4,7 +4,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types'; import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs // tslint:disable: prefer-for-of no-bitwise completed-docs
@ -155,6 +155,22 @@ function dexQuotesToPaths(
return paths; return paths;
} }
export function getTwoHopAdjustedRate(
side: MarketOperation,
twoHopQuote: DexSample<MultiHopFillData>,
targetInput: BigNumber,
ethToOutputRate: BigNumber,
fees: FeeSchedule = {},
): BigNumber {
const { output, input, fillData } = twoHopQuote;
if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT;
}
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData));
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
}
function sourceToFillFlags(source: ERC20BridgeSource): number { function sourceToFillFlags(source: ERC20BridgeSource): number {
switch (source) { switch (source) {
case ERC20BridgeSource.Uniswap: case ERC20BridgeSource.Uniswap:

View File

@ -1,5 +1,4 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { ZERO_AMOUNT } from '@0x/order-utils';
import { RFQTIndicativeQuote } from '@0x/quote-server'; import { RFQTIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, NULL_ADDRESS } from '@0x/utils';
@ -9,11 +8,20 @@ import { MarketOperation } from '../../types';
import { QuoteRequestor } from '../quote_requestor'; import { QuoteRequestor } from '../quote_requestor';
import { difference } from '../utils'; import { difference } from '../utils';
import { QuoteReportGenerator } from './../quote_report_generator'; import { generateQuoteReport } from './../quote_report_generator';
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants'; import {
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
FEE_QUOTE_SOURCES,
ONE_ETHER,
SELL_SOURCES,
ZERO_AMOUNT,
} from './constants';
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
import { getBestTwoHopQuote } from './multihop_utils';
import { import {
createOrdersFromPath, createOrdersFromPath,
createOrdersFromTwoHopSample,
createSignedOrdersFromRfqtIndicativeQuotes, createSignedOrdersFromRfqtIndicativeQuotes,
createSignedOrdersWithFillableAmounts, createSignedOrdersWithFillableAmounts,
getNativeOrderTokens, getNativeOrderTokens,
@ -28,10 +36,13 @@ import {
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketSideLiquidity, MarketSideLiquidity,
OptimizedMarketOrder, OptimizedMarketOrder,
OptimizedOrdersAndQuoteReport, OptimizerResult,
OrderDomain, OrderDomain,
TokenAdjacencyGraph,
} from './types'; } from './types';
// tslint:disable:boolean-naming
/** /**
* Returns a indicative quotes or an empty array if RFQT is not enabled or requested * Returns a indicative quotes or an empty array if RFQT is not enabled or requested
* @param makerAssetData the maker asset data * @param makerAssetData the maker asset data
@ -70,6 +81,7 @@ export class MarketOperationUtils {
private readonly contractAddresses: ContractAddresses, private readonly contractAddresses: ContractAddresses,
private readonly _orderDomain: OrderDomain, private readonly _orderDomain: OrderDomain,
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS, private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
private readonly _tokenAdjacencyGraph: TokenAdjacencyGraph = {},
) { ) {
this._wethAddress = contractAddresses.etherToken.toLowerCase(); this._wethAddress = contractAddresses.etherToken.toLowerCase();
this._multiBridge = contractAddresses.multiBridge.toLowerCase(); this._multiBridge = contractAddresses.multiBridge.toLowerCase();
@ -94,54 +106,62 @@ export class MarketOperationUtils {
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase); const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const {
onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts. // Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry,
makerToken,
takerToken,
),
// Get ETH -> maker token price. // Get ETH -> maker token price.
await DexOrderSampler.ops.getMedianSellRateAsync( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
makerToken, makerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
this._sampler.bancorService,
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
await DexOrderSampler.ops.getMedianSellRateAsync( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
takerToken, takerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
// Get sell quotes for taker -> maker. // Get sell quotes for taker -> maker.
await DexOrderSampler.ops.getSellQuotesAsync( this._sampler.getSellQuotes(
difference( difference(
SELL_SOURCES.concat(this._optionalSources()), SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(ERC20BridgeSource.Balancer), _opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
), ),
makerToken, makerToken,
takerToken, takerToken,
sampleAmounts, sampleAmounts,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
this._sampler.bancorService,
), ),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop)
? DexOrderSampler.constant([])
: this._sampler.getTwoHopSellQuotes(
difference(
SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(ERC20BridgeSource.MultiBridge),
),
makerToken,
takerToken,
takerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
); );
const rfqtPromise = getRfqtIndicativeQuotesAsync( const rfqtPromise = getRfqtIndicativeQuotesAsync(
@ -152,45 +172,33 @@ export class MarketOperationUtils {
_opts, _opts,
); );
const balancerPromise = DexOrderSampler.ops const offChainBalancerPromise = sampleBalancerOffChain
.getSellQuotesAsync( ? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
difference([ERC20BridgeSource.Balancer], _opts.excludedSources), : Promise.resolve([]);
makerToken,
takerToken, const offChainBancorPromise = _opts.excludedSources.includes(ERC20BridgeSource.Bancor)
sampleAmounts, ? Promise.resolve([])
this._wethAddress, : this._sampler.getBancorSellQuotesOffChainAsync(makerToken, takerToken, [takerAmount]);
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
this._sampler.bancorService,
)
.then(async r => this._sampler.executeAsync(r));
const [ const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
[balancerQuotes], offChainBalancerQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); offChainBancorQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainBancorPromise]);
// Attach the LiquidityProvider address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach(
q => (q.fillData = { poolAddress: liquidityProviderAddress }),
);
// Attach the MultiBridge address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }),
);
return { return {
side: MarketOperation.Sell, side: MarketOperation.Sell,
inputAmount: takerAmount, inputAmount: takerAmount,
inputToken: takerToken, inputToken: takerToken,
outputToken: makerToken, outputToken: makerToken,
dexQuotes: dexQuotes.concat(balancerQuotes), dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, offChainBancorQuotes]),
nativeOrders, nativeOrders,
orderFillableAmounts, orderFillableAmounts,
ethToOutputRate: ethToMakerAssetRate, ethToOutputRate: ethToMakerAssetRate,
ethToInputRate: ethToTakerAssetRate, ethToInputRate: ethToTakerAssetRate,
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
twoHopQuotes,
}; };
} }
@ -213,69 +221,72 @@ export class MarketOperationUtils {
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase); const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const {
onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources);
// Call the sampler contract. // Call the sampler contract.
const samplerPromise = this._sampler.executeAsync( const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts. // Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange), this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get the custom liquidity provider from registry. // Get ETH -> makerToken token price.
DexOrderSampler.ops.getLiquidityProviderFromRegistry( this._sampler.getMedianSellRate(
this._liquidityProviderRegistry,
makerToken,
takerToken,
),
// Get ETH -> maker token price.
await DexOrderSampler.ops.getMedianSellRateAsync(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
makerToken, makerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
await DexOrderSampler.ops.getMedianSellRateAsync( this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
takerToken, takerToken,
this._wethAddress, this._wethAddress,
ONE_ETHER, ONE_ETHER,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._multiBridge, this._multiBridge,
this._sampler.bancorService,
), ),
// Get buy quotes for taker -> maker. // Get buy quotes for taker -> maker.
await DexOrderSampler.ops.getBuyQuotesAsync( this._sampler.getBuyQuotes(
difference( difference(
BUY_SOURCES.concat( BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [], this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [],
), ),
_opts.excludedSources.concat([ERC20BridgeSource.Balancer]), _opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
), ),
makerToken, makerToken,
takerToken, takerToken,
sampleAmounts, sampleAmounts,
this._wethAddress, this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
this._sampler.bancorService,
), ),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop)
? DexOrderSampler.constant([])
: this._sampler.getTwoHopBuyQuotes(
difference(
BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS
? [ERC20BridgeSource.LiquidityProvider]
: [],
),
_opts.excludedSources,
),
makerToken,
takerToken,
makerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
); );
const balancerPromise = this._sampler.executeAsync( const offChainBalancerPromise = sampleBalancerOffChain
await DexOrderSampler.ops.getBuyQuotesAsync( ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
difference([ERC20BridgeSource.Balancer], _opts.excludedSources), : Promise.resolve([]);
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._sampler.bancorService,
),
);
const rfqtPromise = getRfqtIndicativeQuotesAsync( const rfqtPromise = getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData, nativeOrders[0].makerAssetData,
@ -285,14 +296,10 @@ export class MarketOperationUtils {
_opts, _opts,
); );
const [ const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes], [orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
[balancerQuotes], offChainBalancerQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]); ] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise]);
// Attach the LiquidityProvider address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach(
q => (q.fillData = { poolAddress: liquidityProviderAddress }),
);
// Attach the MultiBridge address to the sample fillData // Attach the MultiBridge address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach( (dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }), q => (q.fillData = { poolAddress: this._multiBridge }),
@ -302,12 +309,13 @@ export class MarketOperationUtils {
inputAmount: makerAmount, inputAmount: makerAmount,
inputToken: makerToken, inputToken: makerToken,
outputToken: takerToken, outputToken: takerToken,
dexQuotes: dexQuotes.concat(balancerQuotes), dexQuotes: dexQuotes.concat(offChainBalancerQuotes),
nativeOrders, nativeOrders,
orderFillableAmounts, orderFillableAmounts,
ethToOutputRate: ethToTakerAssetRate, ethToOutputRate: ethToTakerAssetRate,
ethToInputRate: ethToMakerAssetRate, ethToInputRate: ethToMakerAssetRate,
rfqtIndicativeQuotes, rfqtIndicativeQuotes,
twoHopQuotes,
}; };
} }
@ -323,7 +331,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
takerAmount: BigNumber, takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> { ): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, { return this._generateOptimizedOrdersAsync(marketSideLiquidity, {
@ -349,7 +357,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
makerAmount: BigNumber, makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> { ): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, { return this._generateOptimizedOrdersAsync(marketSideLiquidity, {
@ -384,35 +392,29 @@ export class MarketOperationUtils {
} }
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const sources = difference(BUY_SOURCES, _opts.excludedSources); const sources = difference(BUY_SOURCES, _opts.excludedSources.concat(ERC20BridgeSource.Balancer));
const ops = [ const ops = [
...batchNativeOrders.map(orders => ...batchNativeOrders.map(orders =>
DexOrderSampler.ops.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange), this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange),
), ),
...(await Promise.all( ...batchNativeOrders.map(orders =>
batchNativeOrders.map(async orders => this._sampler.getMedianSellRate(
DexOrderSampler.ops.getMedianSellRateAsync( difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), getNativeOrderTokens(orders[0])[1],
getNativeOrderTokens(orders[0])[1], this._wethAddress,
this._wethAddress, ONE_ETHER,
ONE_ETHER, this._wethAddress,
this._wethAddress,
this._sampler.balancerPoolsCache,
),
), ),
)), ),
...(await Promise.all( ...batchNativeOrders.map((orders, i) =>
batchNativeOrders.map(async (orders, i) => this._sampler.getBuyQuotes(
DexOrderSampler.ops.getBuyQuotesAsync( sources,
sources, getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[0], getNativeOrderTokens(orders[0])[1],
getNativeOrderTokens(orders[0])[1], [makerAmounts[i]],
[makerAmounts[i]], this._wethAddress,
this._wethAddress,
this._sampler.balancerPoolsCache,
),
), ),
)), ),
]; ];
const executeResults = await this._sampler.executeBatchAsync(ops); const executeResults = await this._sampler.executeBatchAsync(ops);
@ -444,6 +446,7 @@ export class MarketOperationUtils {
rfqtIndicativeQuotes: [], rfqtIndicativeQuotes: [],
inputToken: makerToken, inputToken: makerToken,
outputToken: takerToken, outputToken: takerToken,
twoHopQuotes: [],
}, },
{ {
bridgeSlippage: _opts.bridgeSlippage, bridgeSlippage: _opts.bridgeSlippage,
@ -476,7 +479,7 @@ export class MarketOperationUtils {
shouldBatchBridgeOrders?: boolean; shouldBatchBridgeOrders?: boolean;
quoteRequestor?: QuoteRequestor; quoteRequestor?: QuoteRequestor;
}, },
): Promise<OptimizedOrdersAndQuoteReport> { ): Promise<OptimizerResult> {
const { const {
inputToken, inputToken,
outputToken, outputToken,
@ -488,8 +491,20 @@ export class MarketOperationUtils {
dexQuotes, dexQuotes,
ethToOutputRate, ethToOutputRate,
ethToInputRate, ethToInputRate,
twoHopQuotes,
} = marketSideLiquidity; } = marketSideLiquidity;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0; const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
const orderOpts = {
side,
inputToken,
outputToken,
orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
};
// Convert native orders and dex quotes into fill paths. // Convert native orders and dex quotes into fill paths.
const paths = createFillPaths({ const paths = createFillPaths({
side, side,
@ -505,12 +520,33 @@ export class MarketOperationUtils {
excludedSources: opts.excludedSources, excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule, feeSchedule: opts.feeSchedule,
}); });
// Find the optimal path. // Find the optimal path.
let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || []; let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || [];
if (optimalPath.length === 0) { if (optimalPath.length === 0) {
throw new Error(AggregationError.NoOptimalPath); throw new Error(AggregationError.NoOptimalPath);
} }
// Generate a fallback path if native orders are in the optimal paath. const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount);
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
marketSideLiquidity,
opts.feeSchedule,
);
if (bestTwoHopRate.isGreaterThan(optimalPathRate)) {
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote!, orderOpts);
const twoHopQuoteReport = generateQuoteReport(
side,
_.flatten(dexQuotes),
twoHopQuotes,
nativeOrders,
orderFillableAmounts,
bestTwoHopQuote!,
opts.quoteRequestor,
);
return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true };
}
// Generate a fallback path if native orders are in the optimal path.
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native); const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
if (opts.allowFallback && nativeSubPath.length !== 0) { if (opts.allowFallback && nativeSubPath.length !== 0) {
// We create a fallback path that is exclusive of Native liquidity // We create a fallback path that is exclusive of Native liquidity
@ -519,12 +555,7 @@ export class MarketOperationUtils {
const nonNativeOptimalPath = const nonNativeOptimalPath =
(await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || []; (await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || [];
// Calculate the slippage of on-chain sources compared to the most optimal path // Calculate the slippage of on-chain sources compared to the most optimal path
const fallbackSlippage = getPathAdjustedSlippage( const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate);
side,
nonNativeOptimalPath,
inputAmount,
getPathAdjustedRate(side, optimalPath, inputAmount),
);
if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) { if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) {
// If the last fill is Native and penultimate is not, then the intention was to partial fill // If the last fill is Native and penultimate is not, then the intention was to partial fill
// In this case we drop it entirely as we can't handle a failure at the end and we don't // In this case we drop it entirely as we can't handle a failure at the end and we don't
@ -542,24 +573,17 @@ export class MarketOperationUtils {
optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath];
} }
} }
const optimizedOrders = createOrdersFromPath(optimalPath, { const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts);
side, const quoteReport = generateQuoteReport(
inputToken,
outputToken,
orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
});
const quoteReport = new QuoteReportGenerator(
side, side,
_.flatten(dexQuotes), _.flatten(dexQuotes),
twoHopQuotes,
nativeOrders, nativeOrders,
orderFillableAmounts, orderFillableAmounts,
_.flatten(optimizedOrders.map(o => o.fills)), _.flatten(optimizedOrders.map(order => order.fills)),
opts.quoteRequestor, opts.quoteRequestor,
).generateReport(); );
return { optimizedOrders, quoteReport }; return { optimizedOrders, quoteReport, isTwoHop: false };
} }
private _optionalSources(): ERC20BridgeSource[] { private _optionalSources(): ERC20BridgeSource[] {

View File

@ -0,0 +1,51 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { ZERO_AMOUNT } from './constants';
import { getTwoHopAdjustedRate } from './fills';
import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types';
/**
* Given a token pair, returns the intermediate tokens to consider for two-hop routes.
*/
export function getIntermediateTokens(
makerToken: string,
takerToken: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
wethAddress: string,
): string[] {
let intermediateTokens = [];
if (makerToken === wethAddress) {
intermediateTokens = _.get(tokenAdjacencyGraph, takerToken, [] as string[]);
} else if (takerToken === wethAddress) {
intermediateTokens = _.get(tokenAdjacencyGraph, makerToken, [] as string[]);
} else {
intermediateTokens = _.union(
_.intersection(_.get(tokenAdjacencyGraph, takerToken, []), _.get(tokenAdjacencyGraph, makerToken, [])),
[wethAddress],
);
}
return intermediateTokens.filter(
token => token.toLowerCase() !== makerToken.toLowerCase() && token.toLowerCase() !== takerToken.toLowerCase(),
);
}
/**
* Returns the best two-hop quote and the fee-adjusted rate of that quote.
*/
export function getBestTwoHopQuote(
marketSideLiquidity: MarketSideLiquidity,
feeSchedule?: FeeSchedule,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity;
return twoHopQuotes
.map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule))
.reduce(
(prev, curr, i) =>
curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev,
{
adjustedRate: ZERO_AMOUNT,
quote: undefined as DexSample<MultiHopFillData> | undefined,
},
);
}

View File

@ -8,6 +8,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { import {
ERC20_PROXY_ID, ERC20_PROXY_ID,
MAX_UINT256,
NULL_ADDRESS, NULL_ADDRESS,
NULL_BYTES, NULL_BYTES,
ONE_HOUR_IN_SECONDS, ONE_HOUR_IN_SECONDS,
@ -23,10 +24,12 @@ import {
BancorFillData, BancorFillData,
CollapsedFill, CollapsedFill,
CurveFillData, CurveFillData,
DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill, Fill,
LiquidityProviderFillData, LiquidityProviderFillData,
MultiBridgeFillData, MultiBridgeFillData,
MultiHopFillData,
NativeCollapsedFill, NativeCollapsedFill,
OptimizedMarketOrder, OptimizedMarketOrder,
OrderDomain, OrderDomain,
@ -150,6 +153,7 @@ export interface CreateOrderFromPathOpts {
// Convert sell fills into orders. // Convert sell fills into orders.
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const collapsedPath = collapsePath(path); const collapsedPath = collapsePath(path);
const orders: OptimizedMarketOrder[] = []; const orders: OptimizedMarketOrder[] = [];
for (let i = 0; i < collapsedPath.length; ) { for (let i = 0; i < collapsedPath.length; ) {
@ -168,7 +172,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
} }
// Always use DexForwarderBridge unless configured not to // Always use DexForwarderBridge unless configured not to
if (!opts.shouldBatchBridgeOrders) { if (!opts.shouldBatchBridgeOrders) {
orders.push(createBridgeOrder(contiguousBridgeFills[0], opts)); orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
i += 1; i += 1;
} else { } else {
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
@ -178,9 +182,36 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
return orders; return orders;
} }
export function createOrdersFromTwoHopSample(
sample: DexSample<MultiHopFillData>,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData!;
const firstHopFill: CollapsedFill = {
sourcePathId: '',
source: firstHopSource.source,
input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
subFills: [],
fillData: firstHopSource.fillData,
};
const secondHopFill: CollapsedFill = {
sourcePathId: '',
source: secondHopSource.source,
input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
subFills: [],
fillData: secondHopSource.fillData,
};
return [
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts),
createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts),
];
}
function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string { function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string {
const source = fill.source; switch (fill.source) {
switch (source) {
case ERC20BridgeSource.Eth2Dai: case ERC20BridgeSource.Eth2Dai:
return opts.contractAddresses.eth2DaiBridge; return opts.contractAddresses.eth2DaiBridge;
case ERC20BridgeSource.Kyber: case ERC20BridgeSource.Kyber:
@ -209,8 +240,12 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath
throw new Error(AggregationError.NoBridgeForSource); throw new Error(AggregationError.NoBridgeForSource);
} }
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { function createBridgeOrder(
const [makerToken, takerToken] = getMakerTakerTokens(opts); fill: CollapsedFill,
makerToken: string,
takerToken: string,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder {
const bridgeAddress = getBridgeAddressFromFill(fill, opts); const bridgeAddress = getBridgeAddressFromFill(fill, opts);
let makerAssetData; let makerAssetData;
@ -277,7 +312,7 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
takerAssetAmount: slippedTakerAssetAmount, takerAssetAmount: slippedTakerAssetAmount,
fillableMakerAssetAmount: slippedMakerAssetAmount, fillableMakerAssetAmount: slippedMakerAssetAmount,
fillableTakerAssetAmount: slippedTakerAssetAmount, fillableTakerAssetAmount: slippedTakerAssetAmount,
...createCommonBridgeOrderFields(opts), ...createCommonBridgeOrderFields(opts.orderDomain),
}; };
} }
@ -290,7 +325,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
calls: [], calls: [],
}; };
for (const fill of fills) { for (const fill of fills) {
const bridgeOrder = createBridgeOrder(fill, opts); const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts);
totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount);
totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount);
const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow(
@ -318,7 +353,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
takerAssetAmount: totalTakerAssetAmount, takerAssetAmount: totalTakerAssetAmount,
fillableMakerAssetAmount: totalMakerAssetAmount, fillableMakerAssetAmount: totalMakerAssetAmount,
fillableTakerAssetAmount: totalTakerAssetAmount, fillableTakerAssetAmount: totalTakerAssetAmount,
...createCommonBridgeOrderFields(opts), ...createCommonBridgeOrderFields(opts.orderDomain),
}; };
} }
@ -395,7 +430,7 @@ function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFrom
// Taker asset amount. // Taker asset amount.
opts.side === MarketOperation.Sell opts.side === MarketOperation.Sell
? fill.input ? fill.input
: fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), : BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256),
]; ];
} }
@ -414,7 +449,7 @@ type CommonBridgeOrderFields = Pick<
> >
>; >;
function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields {
return { return {
takerAddress: NULL_ADDRESS, takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS,
@ -428,7 +463,7 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
takerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT, fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE, signature: WALLET_SIGNATURE,
...opts.orderDomain, ...orderDomain,
}; };
} }

View File

@ -5,7 +5,7 @@ import { ERC20BridgeSamplerContract } from '../../wrappers';
import { BalancerPoolsCache } from './balancer_utils'; import { BalancerPoolsCache } from './balancer_utils';
import { BancorService } from './bancor_service'; import { BancorService } from './bancor_service';
import { samplerOperations } from './sampler_operations'; import { SamplerOperations } from './sampler_operations';
import { BatchedOperation } from './types'; import { BatchedOperation } from './types';
/** /**
@ -30,19 +30,15 @@ type BatchedOperationResult<T> = T extends BatchedOperation<infer TResult> ? TRe
/** /**
* Encapsulates interactions with the `ERC20BridgeSampler` contract. * Encapsulates interactions with the `ERC20BridgeSampler` contract.
*/ */
export class DexOrderSampler { export class DexOrderSampler extends SamplerOperations {
/**
* Composable operations that can be batched in a single transaction,
* for use with `DexOrderSampler.executeAsync()`.
*/
public static ops = samplerOperations;
constructor( constructor(
private readonly _samplerContract: ERC20BridgeSamplerContract, _samplerContract: ERC20BridgeSamplerContract,
private readonly _samplerOverrides?: SamplerOverrides, private readonly _samplerOverrides?: SamplerOverrides,
public bancorService?: BancorService, bancorService?: BancorService,
public balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), balancerPoolsCache?: BalancerPoolsCache,
) {} ) {
super(_samplerContract, bancorService, balancerPoolsCache);
}
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */
@ -142,16 +138,14 @@ export class DexOrderSampler {
* Takes an arbitrary length array, but is not typesafe. * Takes an arbitrary length array, but is not typesafe.
*/ */
public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> { public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const callDatas = ops.map(o => o.encodeCall(this._samplerContract)); const callDatas = ops.map(o => o.encodeCall());
const { overrides, block } = this._samplerOverrides const { overrides, block } = this._samplerOverrides
? this._samplerOverrides ? this._samplerOverrides
: { overrides: undefined, block: undefined }; : { overrides: undefined, block: undefined };
// All operations are NOOPs // All operations are NOOPs
if (callDatas.every(cd => cd === NULL_BYTES)) { if (callDatas.every(cd => cd === NULL_BYTES)) {
return Promise.all( return callDatas.map((_callData, i) => ops[i].handleCallResults(NULL_BYTES));
callDatas.map(async (_callData, i) => ops[i].handleCallResultsAsync(this._samplerContract, NULL_BYTES)),
);
} }
// Execute all non-empty calldatas. // Execute all non-empty calldatas.
const rawCallResults = await this._samplerContract const rawCallResults = await this._samplerContract
@ -159,11 +153,9 @@ export class DexOrderSampler {
.callAsync({ overrides }, block); .callAsync({ overrides }, block);
// Return the parsed results. // Return the parsed results.
let rawCallResultsIdx = 0; let rawCallResultsIdx = 0;
return Promise.all( return callDatas.map((callData, i) => {
callDatas.map(async (callData, i) => { const result = callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : NULL_BYTES;
const result = callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : NULL_BYTES; return ops[i].handleCallResults(result);
return ops[i].handleCallResultsAsync(this._samplerContract, result); });
}),
);
} }
} }

View File

@ -0,0 +1,52 @@
import { ContractFunctionObj } from '@0x/base-contract';
import { BigNumber } from '@0x/utils';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { ERC20BridgeSource, FillData, SourceInfo, SourceQuoteOperation } from './types';
export type Parameters<T> = T extends (...args: infer TArgs) => any ? TArgs : never;
export interface SamplerContractCall<
TFunc extends (...args: any[]) => ContractFunctionObj<any>,
TFillData extends FillData = FillData
> {
contract: ERC20BridgeSamplerContract;
function: TFunc;
params: Parameters<TFunc>;
callback?: (callResults: string, fillData: TFillData) => BigNumber[];
}
export class SamplerContractOperation<
TFunc extends (...args: any[]) => ContractFunctionObj<any>,
TFillData extends FillData = FillData
> implements SourceQuoteOperation<TFillData> {
public readonly source: ERC20BridgeSource;
public fillData: TFillData;
private readonly _samplerContract: ERC20BridgeSamplerContract;
private readonly _samplerFunction: TFunc;
private readonly _params: Parameters<TFunc>;
private readonly _callback?: (callResults: string, fillData: TFillData) => BigNumber[];
constructor(opts: SourceInfo<TFillData> & SamplerContractCall<TFunc, TFillData>) {
this.source = opts.source;
this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion
this._samplerContract = opts.contract;
this._samplerFunction = opts.function;
this._params = opts.params;
this._callback = opts.callback;
}
public encodeCall(): string {
return this._samplerFunction
.bind(this._samplerContract)(...this._params)
.getABIEncodedTransactionData();
}
public handleCallResults(callResults: string): BigNumber[] {
if (this._callback !== undefined) {
return this._callback(callResults, this.fillData);
} else {
return this._samplerContract.getABIDecodedReturnData<BigNumber[]>(this._samplerFunction.name, callResults);
}
}
}

View File

@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils';
import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteRequestor } from '../../utils/quote_requestor';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { QuoteReport } from '../quote_report_generator'; import { QuoteReport } from '../quote_report_generator';
/** /**
@ -41,6 +40,7 @@ export enum ERC20BridgeSource {
Bancor = 'Bancor', Bancor = 'Bancor',
MStable = 'mStable', MStable = 'mStable',
Mooniswap = 'Mooniswap', Mooniswap = 'Mooniswap',
MultiHop = 'MultiHop',
} }
// tslint:disable: enum-naming // tslint:disable: enum-naming
@ -72,6 +72,11 @@ export interface CurveInfo {
// Internal `fillData` field for `Fill` objects. // Internal `fillData` field for `Fill` objects.
export interface FillData {} export interface FillData {}
export interface SourceInfo<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData?: TFillData;
}
// `FillData` for native fills. // `FillData` for native fills.
export interface NativeFillData extends FillData { export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts; order: SignedOrderWithFillableAmounts;
@ -108,14 +113,23 @@ export interface Quote<TFillData = FillData> {
fillData?: TFillData; fillData?: TFillData;
} }
export interface HopInfo {
sourceIndex: BigNumber;
returnData: string;
}
export interface MultiHopFillData extends FillData {
firstHopSource: SourceQuoteOperation;
secondHopSource: SourceQuoteOperation;
intermediateToken: string;
}
/** /**
* Represents an individual DEX sample from the sampler contract. * Represents an individual DEX sample from the sampler contract.
*/ */
export interface DexSample<TFillData extends FillData = FillData> { export interface DexSample<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
source: ERC20BridgeSource;
input: BigNumber; input: BigNumber;
output: BigNumber; output: BigNumber;
fillData?: TFillData;
} }
/** /**
@ -131,7 +145,7 @@ export enum FillFlags {
/** /**
* Represents a node on a fill path. * Represents a node on a fill path.
*/ */
export interface Fill<TFillData extends FillData = FillData> { export interface Fill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
// Unique ID of the original source path this fill belongs to. // Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish // This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves). // paths that have the same `source` IDs but are distinct (e.g., Curves).
@ -148,25 +162,16 @@ export interface Fill<TFillData extends FillData = FillData> {
parent?: Fill; parent?: Fill;
// The index of the fill in the original path. // The index of the fill in the original path.
index: number; index: number;
// The source of the fill. See `ERC20BridgeSource`.
source: ERC20BridgeSource;
// Data associated with this this Fill object. Used to reconstruct orders
// from paths.
fillData?: TFillData;
} }
/** /**
* Represents continguous fills on a path that have been merged together. * Represents continguous fills on a path that have been merged together.
*/ */
export interface CollapsedFill<TFillData extends FillData = FillData> { export interface CollapsedFill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
// Unique ID of the original source path this fill belongs to. // Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish // This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves). // paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string; sourcePathId: string;
/**
* The source DEX.
*/
source: ERC20BridgeSource;
/** /**
* Total input amount (sum of `subFill`s) * Total input amount (sum of `subFill`s)
*/ */
@ -182,8 +187,6 @@ export interface CollapsedFill<TFillData extends FillData = FillData> {
input: BigNumber; input: BigNumber;
output: BigNumber; output: BigNumber;
}>; }>;
fillData?: TFillData;
} }
/** /**
@ -273,17 +276,19 @@ export interface GetMarketOrdersOpts {
* A composable operation the be run in `DexOrderSampler.executeAsync()`. * A composable operation the be run in `DexOrderSampler.executeAsync()`.
*/ */
export interface BatchedOperation<TResult> { export interface BatchedOperation<TResult> {
encodeCall(contract: ERC20BridgeSamplerContract): string; encodeCall(): string;
handleCallResultsAsync(contract: ERC20BridgeSamplerContract, callResults: string): Promise<TResult>; handleCallResults(callResults: string): TResult;
} }
export interface SourceQuoteOperation<TFillData extends FillData = FillData> export interface SourceQuoteOperation<TFillData extends FillData = FillData>
extends BatchedOperation<Array<Quote<TFillData>>> { extends BatchedOperation<BigNumber[]>,
source: ERC20BridgeSource; SourceInfo<TFillData> {
readonly source: ERC20BridgeSource;
} }
export interface OptimizedOrdersAndQuoteReport { export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[]; optimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean;
quoteReport: QuoteReport; quoteReport: QuoteReport;
} }
@ -305,4 +310,9 @@ export interface MarketSideLiquidity {
ethToOutputRate: BigNumber; ethToOutputRate: BigNumber;
ethToInputRate: BigNumber; ethToInputRate: BigNumber;
rfqtIndicativeQuotes: RFQTIndicativeQuote[]; rfqtIndicativeQuotes: RFQTIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>;
}
export interface TokenAdjacencyGraph {
[token: string]: string[];
} }

View File

@ -1,11 +1,17 @@
import { orderHashUtils } from '@0x/order-utils'; import { orderHashUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ERC20BridgeSource, SignedOrder } from '..';
import { MarketOperation } from '../types'; import { MarketOperation } from '../types';
import { CollapsedFill, DexSample, NativeCollapsedFill } from './market_operation_utils/types'; import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
} from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor'; import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource { export interface BridgeReportSource {
@ -14,6 +20,13 @@ export interface BridgeReportSource {
takerAmount: BigNumber; takerAmount: BigNumber;
} }
export interface MultiHopReportSource {
liquiditySource: ERC20BridgeSource.MultiHop;
makerAmount: BigNumber;
takerAmount: BigNumber;
hopSources: ERC20BridgeSource[];
}
interface NativeReportSourceBase { interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native; liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber; makerAmount: BigNumber;
@ -29,7 +42,11 @@ export interface NativeRFQTReportSource extends NativeReportSourceBase {
isRfqt: true; isRfqt: true;
makerUri: string; makerUri: string;
} }
export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource; export type QuoteReportSource =
| BridgeReportSource
| NativeOrderbookReportSource
| NativeRFQTReportSource
| MultiHopReportSource;
export interface QuoteReport { export interface QuoteReport {
sourcesConsidered: QuoteReportSource[]; sourcesConsidered: QuoteReportSource[];
@ -47,115 +64,152 @@ const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefine
} }
}; };
export class QuoteReportGenerator { /**
private readonly _dexQuotes: DexSample[]; * Generates a report of sources considered while computing the optimized
private readonly _nativeOrders: SignedOrder[]; * swap quote, and the sources ultimately included in the computed quote.
private readonly _orderHashesToFillableAmounts: { [orderHash: string]: BigNumber }; */
private readonly _marketOperation: MarketOperation; export function generateQuoteReport(
private readonly _collapsedFills: CollapsedFill[]; marketOperation: MarketOperation,
private readonly _quoteRequestor?: QuoteRequestor; dexQuotes: DexSample[],
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
constructor( nativeOrders: SignedOrder[],
marketOperation: MarketOperation, orderFillableAmounts: BigNumber[],
dexQuotes: DexSample[], liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>,
nativeOrders: SignedOrder[], quoteRequestor?: QuoteRequestor,
orderFillableAmounts: BigNumber[], ): QuoteReport {
collapsedFills: CollapsedFill[], // convert order fillable amount array to easy to look up hash
quoteRequestor?: QuoteRequestor, if (orderFillableAmounts.length !== nativeOrders.length) {
) { // length mismatch, abort
this._dexQuotes = dexQuotes; throw new Error('orderFillableAmounts must be the same length as nativeOrders');
this._nativeOrders = nativeOrders;
this._marketOperation = marketOperation;
this._quoteRequestor = quoteRequestor;
this._collapsedFills = collapsedFills;
// convert order fillable amount array to easy to look up hash
if (orderFillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
this._orderHashesToFillableAmounts = {};
return;
}
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
this._orderHashesToFillableAmounts = orderHashesToFillableAmounts;
} }
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
public generateReport(): QuoteReport { const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq)); const nativeOrderSourcesConsidered = nativeOrders.map(order =>
const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no)); _nativeOrderToReportSource(
order,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(order)],
quoteRequestor,
),
);
const multiHopSourcesConsidered = multiHopQuotes.map(quote =>
_multiHopSampleToReportSource(quote, marketOperation),
);
const sourcesConsidered = [
...dexReportSourcesConsidered,
...nativeOrderSourcesConsidered,
...multiHopSourcesConsidered,
];
const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered]; let sourcesDelivered;
const sourcesDelivered = this._collapsedFills.map(collapsedFill => { if (Array.isArray(liquidityDelivered)) {
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill); const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) { if (foundNativeOrder) {
return this._nativeOrderToReportSource(foundNativeOrder); return _nativeOrderToReportSource(
foundNativeOrder,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(foundNativeOrder)],
quoteRequestor,
);
} else { } else {
return this._dexSampleToReportSource(collapsedFill); return _dexSampleToReportSource(collapsedFill, marketOperation);
} }
}); });
} else {
sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)];
}
return {
sourcesConsidered,
sourcesDelivered,
};
}
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeReportSource {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return { return {
sourcesConsidered, makerAmount: ds.input,
sourcesDelivered, takerAmount: ds.output,
liquiditySource,
}; };
} } else if (marketOperation === MarketOperation.Sell) {
return {
private _dexSampleToReportSource(ds: DexSample): BridgeReportSource { makerAmount: ds.output,
const liquiditySource = ds.source; takerAmount: ds.input,
liquiditySource,
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (this._marketOperation === MarketOperation.Buy) {
return {
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
};
} else if (this._marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
};
} else {
throw new Error(`Unexpected marketOperation ${this._marketOperation}`);
}
}
private _nativeOrderToReportSource(nativeOrder: SignedOrder): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: this._orderHashesToFillableAmounts[orderHash],
nativeOrder,
orderHash,
}; };
} else {
// if we find this is an rfqt order, label it as such and associate makerUri throw new Error(`Unexpected marketOperation ${marketOperation}`);
const foundRfqtMakerUri = this._quoteRequestor && this._quoteRequestor.getMakerUriForOrderHash(orderHash); }
if (foundRfqtMakerUri) { }
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase, function _multiHopSampleToReportSource(
isRfqt: true, ds: DexSample<MultiHopFillData>,
makerUri: foundRfqtMakerUri, marketOperation: MarketOperation,
}; ): MultiHopReportSource {
return rfqtSource; const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!;
} else { // input and output map to different values
// if it's not an rfqt order, treat as normal // based on the market operation
const regularNativeOrder: NativeOrderbookReportSource = { if (marketOperation === MarketOperation.Buy) {
...nativeOrderBase, return {
isRfqt: false, liquiditySource: ERC20BridgeSource.MultiHop,
}; makerAmount: ds.input,
return regularNativeOrder; takerAmount: ds.output,
} hopSources: [firstHop.source, secondHop.source],
};
} else if (marketOperation === MarketOperation.Sell) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.output,
takerAmount: ds.input,
hopSources: [firstHop.source, secondHop.source],
};
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
function _nativeOrderToReportSource(
nativeOrder: SignedOrder,
fillableAmount: BigNumber,
quoteRequestor?: QuoteRequestor,
): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: fillableAmount,
nativeOrder,
orderHash,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderHash(orderHash);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
return rfqtSource;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
} }
} }

View File

@ -355,10 +355,8 @@ export class QuoteRequestor {
switch (quoteType) { switch (quoteType) {
case 'firm': case 'firm':
return 'quote'; return 'quote';
break;
case 'indicative': case 'indicative':
return 'price'; return 'price';
break;
default: default:
throw new Error(`Unexpected quote type ${quoteType}`); throw new Error(`Unexpected quote type ${quoteType}`);
} }

View File

@ -100,10 +100,11 @@ export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult
...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS,
...quoteInfo.opts, ...quoteInfo.opts,
}; };
const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier);
const result = fillQuoteOrders( const result = fillQuoteOrders(
createBestCaseFillOrderCalls(quoteInfo), createBestCaseFillOrderCalls(quoteInfo),
quoteInfo.fillAmount, quoteInfo.fillAmount,
quoteInfo.gasPrice.times(opts.protocolFeeMultiplier), protocolFeePerFillOrder,
opts.gasSchedule, opts.gasSchedule,
); );
return fromIntermediateQuoteFillResult(result, quoteInfo); return fromIntermediateQuoteFillResult(result, quoteInfo);

View File

@ -3,6 +3,7 @@ import { AssetProxyId, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants } from '../constants';
import { import {
CalculateSwapQuoteOpts, CalculateSwapQuoteOpts,
MarketBuySwapQuote, MarketBuySwapQuote,
@ -16,8 +17,14 @@ import {
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; import {
import { isSupportedAssetDataInOrders } from './utils'; ERC20BridgeSource,
FeeSchedule,
FillData,
GetMarketOrdersOpts,
OptimizedMarketOrder,
} from './market_operation_utils/types';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator'; import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
@ -121,8 +128,9 @@ export class SwapQuoteCalculator {
} }
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
let optimizedOrders: OptimizedMarketOrder[] | undefined; let optimizedOrders: OptimizedMarketOrder[];
let quoteReport: QuoteReport | undefined; let quoteReport: QuoteReport | undefined;
let isTwoHop = false;
{ {
// Scale fees by gas price. // Scale fees by gas price.
@ -149,6 +157,7 @@ export class SwapQuoteCalculator {
); );
optimizedOrders = buyResult.optimizedOrders; optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport; quoteReport = buyResult.quoteReport;
isTwoHop = buyResult.isTwoHop;
} else { } else {
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders, prunedOrders,
@ -157,22 +166,34 @@ export class SwapQuoteCalculator {
); );
optimizedOrders = sellResult.optimizedOrders; optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport; quoteReport = sellResult.quoteReport;
isTwoHop = sellResult.isTwoHop;
} }
} }
} }
// assetData information for the result // assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0]; const { makerAssetData, takerAssetData } = prunedOrders[0];
return createSwapQuote( return isTwoHop
makerAssetData, ? createTwoHopSwapQuote(
takerAssetData, makerAssetData,
optimizedOrders, takerAssetData,
operation, optimizedOrders,
assetFillAmount, operation,
gasPrice, assetFillAmount,
opts.gasSchedule, gasPrice,
quoteReport, opts.gasSchedule,
); quoteReport,
)
: createSwapQuote(
makerAssetData,
takerAssetData,
optimizedOrders,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
quoteReport,
);
} }
} }
@ -211,6 +232,74 @@ function createSwapQuote(
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
orders: optimizedOrders, orders: optimizedOrders,
quoteReport, quoteReport,
isTwoHop: false,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
function createTwoHopSwapQuote(
makerAssetData: string,
takerAssetData: string,
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
quoteReport?: QuoteReport,
): SwapQuote {
const [firstHopOrder, secondHopOrder] = optimizedOrders;
const [firstHopFill] = firstHopOrder.fills;
const [secondHopFill] = secondHopOrder.fills;
const gas = new BigNumber(
gasSchedule[ERC20BridgeSource.MultiHop]!({
firstHopSource: _.pick(firstHopFill, 'source', 'fillData'),
secondHopSource: _.pick(secondHopFill, 'source', 'fillData'),
}),
).toNumber();
const quoteBase = {
takerAssetData,
makerAssetData,
gasPrice,
bestCaseQuoteInfo: {
makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input,
takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
totalTakerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
worstCaseQuoteInfo: {
makerAssetAmount: secondHopOrder.makerAssetAmount,
takerAssetAmount: firstHopOrder.takerAssetAmount,
totalTakerAssetAmount: firstHopOrder.takerAssetAmount,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
sourceBreakdown: {
[ERC20BridgeSource.MultiHop]: {
proportion: new BigNumber(1),
intermediateToken: getTokenFromAssetData(secondHopOrder.takerAssetData),
hops: [firstHopFill.source, secondHopFill.source],
},
},
orders: optimizedOrders,
quoteReport,
isTwoHop: true,
}; };
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
@ -218,14 +307,12 @@ function createSwapQuote(
...quoteBase, ...quoteBase,
type: MarketOperation.Buy, type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount, makerAssetFillAmount: assetFillAmount,
quoteReport,
}; };
} else { } else {
return { return {
...quoteBase, ...quoteBase,
type: MarketOperation.Sell, type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount, takerAssetFillAmount: assetFillAmount,
quoteReport,
}; };
} }
} }
@ -234,7 +321,7 @@ function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: Big
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
const breakdown: SwapQuoteOrdersBreakdown = {}; const breakdown: SwapQuoteOrdersBreakdown = {};
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
breakdown[source] = fillAmount.div(totalFillAmount); breakdown[source as keyof SwapQuoteOrdersBreakdown] = fillAmount.div(totalFillAmount);
}); });
return breakdown; return breakdown;
} }

View File

@ -113,3 +113,12 @@ export function isERC20EquivalentAssetData(assetData: AssetData): assetData is E
export function difference<T>(a: T[], b: T[]): T[] { export function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1); return a.filter(x => b.indexOf(x) === -1);
} }
export function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}

View File

@ -6,11 +6,13 @@
import { ContractArtifact } from 'ethereum-types'; import { ContractArtifact } from 'ethereum-types';
import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json'; import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json';
import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json';
import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json';
import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json'; import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json';
import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json'; import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json';
import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json'; import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json';
import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json'; import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json';
import * as IBalancer from '../test/generated-artifacts/IBalancer.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json'; import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json'; import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json';
import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json'; import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json';
@ -33,15 +35,27 @@ import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSamp
import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json'; import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json';
import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json'; import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json';
import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json';
import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json'; import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json';
import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json'; import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json';
export const artifacts = { export const artifacts = {
ApproximateBuys: ApproximateBuys as ContractArtifact, ApproximateBuys: ApproximateBuys as ContractArtifact,
BalancerSampler: BalancerSampler as ContractArtifact,
CurveSampler: CurveSampler as ContractArtifact, CurveSampler: CurveSampler as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact,
Eth2DaiSampler: Eth2DaiSampler as ContractArtifact, Eth2DaiSampler: Eth2DaiSampler as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact,
KyberSampler: KyberSampler as ContractArtifact,
LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact,
MStableSampler: MStableSampler as ContractArtifact,
MooniswapSampler: MooniswapSampler as ContractArtifact,
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact,
TwoHopSampler: TwoHopSampler as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
IBalancer: IBalancer as ContractArtifact,
ICurve: ICurve as ContractArtifact, ICurve: ICurve as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact,
IKyberHintHandler: IKyberHintHandler as ContractArtifact, IKyberHintHandler: IKyberHintHandler as ContractArtifact,
@ -51,19 +65,11 @@ export const artifacts = {
ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact,
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IMStable: IMStable as ContractArtifact, IMStable: IMStable as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact,
IMultiBridge: IMultiBridge as ContractArtifact, IMultiBridge: IMultiBridge as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
KyberSampler: KyberSampler as ContractArtifact, DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact, DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
MStableSampler: MStableSampler as ContractArtifact,
MooniswapSampler: MooniswapSampler as ContractArtifact,
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact,
TestNativeOrderSampler: TestNativeOrderSampler as ContractArtifact, TestNativeOrderSampler: TestNativeOrderSampler as ContractArtifact,
}; };

View File

@ -37,6 +37,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR';
const MAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
const INTERMEDIATE_TOKEN = randomAddress();
before(async () => { before(async () => {
testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync( testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync(
@ -262,6 +263,33 @@ blockchainTests('erc20-bridge-sampler', env => {
await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 }); await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 });
} }
function expectQuotesWithinRange(
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
): void {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
}
describe('getOrderFillableTakerAssetAmounts()', () => { describe('getOrderFillableTakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => { it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
@ -385,32 +413,6 @@ blockchainTests('erc20-bridge-sampler', env => {
const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync(); const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync();
expect(quotes).to.deep.eq([]); expect(quotes).to.deep.eq([]);
}); });
const expectQuotesWithinRange = (
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
) => {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
};
it('can quote token -> token', async () => { it('can quote token -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
@ -803,7 +805,7 @@ blockchainTests('erc20-bridge-sampler', env => {
}); });
}); });
describe('getLiquidityProviderFromRegistry', () => { describe('liquidity provider', () => {
const xAsset = randomAddress(); const xAsset = randomAddress();
const yAsset = randomAddress(); const yAsset = randomAddress();
const sampleAmounts = getSampleAmounts(yAsset); const sampleAmounts = getSampleAmounts(yAsset);
@ -829,42 +831,28 @@ blockchainTests('erc20-bridge-sampler', env => {
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
}); });
it('should be able to get the liquidity provider', async () => {
const xyLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, xAsset, yAsset)
.callAsync();
const yxLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, xAsset)
.callAsync();
const unknownLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, randomAddress())
.callAsync();
expect(xyLiquidityProvider).to.eq(liquidityProvider.address);
expect(yxLiquidityProvider).to.eq(liquidityProvider.address);
expect(unknownLiquidityProvider).to.eq(constants.NULL_ADDRESS);
});
it('should be able to query sells from the liquidity provider', async () => { it('should be able to query sells from the liquidity provider', async () => {
const result = await testContract const [quotes, providerAddress] = await testContract
.sampleSellsFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts) .sampleSellsFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync(); .callAsync();
result.forEach((value, idx) => { quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].minus(1)); expect(value).is.bignumber.eql(sampleAmounts[idx].minus(1));
}); });
expect(providerAddress).to.equal(liquidityProvider.address);
}); });
it('should be able to query buys from the liquidity provider', async () => { it('should be able to query buys from the liquidity provider', async () => {
const result = await testContract const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts) .sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync(); .callAsync();
result.forEach((value, idx) => { quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1)); expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1));
}); });
expect(providerAddress).to.equal(liquidityProvider.address);
}); });
it('should just return zeros if the liquidity provider cannot be found', async () => { it('should just return zeros if the liquidity provider cannot be found', async () => {
const result = await testContract const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry( .sampleBuysFromLiquidityProviderRegistry(
registryContract.address, registryContract.address,
yAsset, yAsset,
@ -872,18 +860,20 @@ blockchainTests('erc20-bridge-sampler', env => {
sampleAmounts, sampleAmounts,
) )
.callAsync(); .callAsync();
result.forEach(value => { quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT); expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
}); });
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
}); });
it('should just return zeros if the registry does not exist', async () => { it('should just return zeros if the registry does not exist', async () => {
const result = await testContract const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts) .sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts)
.callAsync(); .callAsync();
result.forEach(value => { quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT); expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
}); });
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
}); });
}); });
@ -1033,4 +1023,116 @@ blockchainTests('erc20-bridge-sampler', env => {
); );
}); });
}); });
blockchainTests.resets('TwoHopSampler', () => {
before(async () => {
await testContract
.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN, INTERMEDIATE_TOKEN])
.awaitTransactionSuccessAsync();
});
it('sampleTwoHopSell', async () => {
// tslint:disable-next-line no-unnecessary-type-assertion
const sellAmount = _.last(getSampleAmounts(TAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleSellsFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleSellsFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract
.sampleSellsFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiSecondHop = testContract
.sampleSellsFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const firstHopQuotes = [
getDeterministicSellQuote(ETH2DAI_SALT, TAKER_TOKEN, INTERMEDIATE_TOKEN, sellAmount),
getDeterministicUniswapV2SellQuote(uniswapV2FirstHopPath, sellAmount),
];
const expectedIntermediateAssetAmount = BigNumber.max(...firstHopQuotes);
const secondHopQuotes = [
getDeterministicSellQuote(
ETH2DAI_SALT,
INTERMEDIATE_TOKEN,
MAKER_TOKEN,
expectedIntermediateAssetAmount,
),
getDeterministicUniswapV2SellQuote(uniswapV2SecondHopPath, expectedIntermediateAssetAmount),
];
const expectedBuyAmount = BigNumber.max(...secondHopQuotes);
const [firstHop, secondHop, buyAmount] = await testContract
.sampleTwoHopSell(
[eth2DaiFirstHop, uniswapV2FirstHop],
[eth2DaiSecondHop, uniswapV2SecondHop],
sellAmount,
)
.callAsync();
expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal(
firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)),
);
expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal(
secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedBuyAmount)),
);
expect(buyAmount, 'Two hop buy amount').to.bignumber.equal(expectedBuyAmount);
});
it('sampleTwoHopBuy', async () => {
// tslint:disable-next-line no-unnecessary-type-assertion
const buyAmount = _.last(getSampleAmounts(MAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleBuysFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleBuysFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract
.sampleBuysFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiSecondHop = testContract
.sampleBuysFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const secondHopQuotes = [
getDeterministicBuyQuote(ETH2DAI_SALT, INTERMEDIATE_TOKEN, MAKER_TOKEN, buyAmount),
getDeterministicUniswapV2BuyQuote(uniswapV2SecondHopPath, buyAmount),
];
const expectedIntermediateAssetAmount = BigNumber.min(...secondHopQuotes);
const firstHopQuotes = [
getDeterministicBuyQuote(
ETH2DAI_SALT,
TAKER_TOKEN,
INTERMEDIATE_TOKEN,
expectedIntermediateAssetAmount,
),
getDeterministicUniswapV2BuyQuote(uniswapV2FirstHopPath, expectedIntermediateAssetAmount),
];
const expectedSellAmount = BigNumber.min(...firstHopQuotes);
const [firstHop, secondHop, sellAmount] = await testContract
.sampleTwoHopBuy(
[eth2DaiFirstHop, uniswapV2FirstHop],
[eth2DaiSecondHop, uniswapV2SecondHop],
buyAmount,
)
.callAsync();
expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal(
firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedSellAmount)),
);
expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal(
secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)),
);
expect(sellAmount, 'Two hop sell amount').to.bignumber.equal(expectedSellAmount);
});
});
}); });

View File

@ -18,7 +18,7 @@ import {
computeBalancerSellQuote, computeBalancerSellQuote,
} from '../src/utils/market_operation_utils/balancer_utils'; } from '../src/utils/market_operation_utils/balancer_utils';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types'; import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache'; import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockBancorService } from './utils/mock_bancor_service'; import { MockBancorService } from './utils/mock_bancor_service';
@ -108,7 +108,7 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
); );
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
}); });
@ -124,7 +124,7 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
); );
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts); expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
}); });
@ -144,36 +144,32 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getKyberSellQuotes( dexOrderSampler.getKyberSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
}); });
it('getLiquidityProviderSellQuotes()', async () => { it('getLiquidityProviderSellQuotes()', async () => {
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const registry = randomAddress(); const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({ const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry); expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken); expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken); expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(1001)]; return [[toBaseUnitAmount(1001)], poolAddress];
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync( dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.LiquidityProvider], [ERC20BridgeSource.LiquidityProvider],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
registry, registry,
), ),
); );
@ -183,7 +179,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider', source: 'LiquidityProvider',
output: toBaseUnitAmount(1001), output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined, fillData: { poolAddress },
}, },
], ],
]); ]);
@ -193,23 +189,23 @@ describe('DexSampler tests', () => {
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const registry = randomAddress(); const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({ const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => { sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry); expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken); expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken); expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(999)]; return [[toBaseUnitAmount(999)], poolAddress];
}, },
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync( dexOrderSampler.getBuyQuotes(
[ERC20BridgeSource.LiquidityProvider], [ERC20BridgeSource.LiquidityProvider],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
registry, registry,
), ),
); );
@ -219,7 +215,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider', source: 'LiquidityProvider',
output: toBaseUnitAmount(999), output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined, fillData: { poolAddress },
}, },
], ],
]); ]);
@ -246,13 +242,12 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync( const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync( dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.MultiBridge], [ERC20BridgeSource.MultiBridge],
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
[toBaseUnitAmount(1000)], [toBaseUnitAmount(1000)],
randomAddress(), randomAddress(),
dexOrderSampler.balancerPoolsCache,
randomAddress(), randomAddress(),
multiBridge, multiBridge,
), ),
@ -263,7 +258,7 @@ describe('DexSampler tests', () => {
source: 'MultiBridge', source: 'MultiBridge',
output: toBaseUnitAmount(1001), output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000), input: toBaseUnitAmount(1000),
fillData: undefined, fillData: { poolAddress: multiBridge },
}, },
], ],
]); ]);
@ -284,13 +279,9 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiSellQuotes( dexOrderSampler.getEth2DaiSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
}); });
it('getUniswapSellQuotes()', async () => { it('getUniswapSellQuotes()', async () => {
@ -308,13 +299,9 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapSellQuotes( dexOrderSampler.getUniswapSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
}); });
it('getUniswapV2SellQuotes()', async () => { it('getUniswapV2SellQuotes()', async () => {
@ -331,12 +318,12 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapV2SellQuotes( dexOrderSampler.getUniswapV2SellQuotes(
[expectedMakerToken, expectedTakerToken], [expectedMakerToken, expectedTakerToken],
expectedTakerFillAmounts, expectedTakerFillAmounts,
), ),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
}); });
it('getEth2DaiBuyQuotes()', async () => { it('getEth2DaiBuyQuotes()', async () => {
@ -354,13 +341,9 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiBuyQuotes( dexOrderSampler.getEth2DaiBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
}); });
it('getUniswapBuyQuotes()', async () => { it('getUniswapBuyQuotes()', async () => {
@ -378,13 +361,9 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapBuyQuotes( dexOrderSampler.getUniswapBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
); );
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts); expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
}); });
interface RatesBySource { interface RatesBySource {
@ -440,13 +419,12 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync( dexOrderSampler.getSellQuotes(
sources, sources,
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedTakerFillAmounts, expectedTakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
const expectedQuotes = sources.map(s => const expectedQuotes = sources.map(s =>
@ -457,7 +435,7 @@ describe('DexSampler tests', () => {
fillData: fillData:
s === ERC20BridgeSource.UniswapV2 s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData), : {},
})), })),
); );
const uniswapV2ETHQuotes = [ const uniswapV2ETHQuotes = [
@ -488,19 +466,14 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler( const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}), new MockSamplerContract({}),
undefined, // sampler overrides undefined,
undefined, // bancor service undefined,
balancerPoolsCache, balancerPoolsCache,
); );
const [quotes] = await dexOrderSampler.executeAsync( const quotes = await dexOrderSampler.getBalancerSellQuotesOffChainAsync(
await DexOrderSampler.ops.getSellQuotesAsync( expectedMakerToken,
[ERC20BridgeSource.Balancer], expectedTakerToken,
expectedMakerToken, expectedTakerFillAmounts,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
); );
const expectedQuotes = pools.map(p => const expectedQuotes = pools.map(p =>
expectedTakerFillAmounts.map(a => ({ expectedTakerFillAmounts.map(a => ({
@ -535,28 +508,17 @@ describe('DexSampler tests', () => {
bancorService, bancorService,
undefined, // balancer cache undefined, // balancer cache
); );
const [quotes] = await dexOrderSampler.executeAsync( const quotes = await dexOrderSampler.getBancorSellQuotesOffChainAsync(
await DexOrderSampler.ops.getSellQuotesAsync( expectedMakerToken,
[ERC20BridgeSource.Bancor], expectedTakerToken,
expectedMakerToken, expectedTakerFillAmounts,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
undefined, // balancer pools cache
undefined, // liquidity provider registry address
undefined, // multibridge address
bancorService,
),
); );
const expectedQuotes = [ const expectedQuotes = expectedTakerFillAmounts.map(a => ({
expectedTakerFillAmounts.map(a => ({ source: ERC20BridgeSource.Bancor,
source: ERC20BridgeSource.Bancor, input: a,
input: a, output: a.multipliedBy(rate),
output: a.multipliedBy(rate), fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress },
fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress }, }));
})),
];
expect(quotes).to.have.lengthOf(1); // one set per pool
expect(quotes).to.deep.eq(expectedQuotes); expect(quotes).to.deep.eq(expectedQuotes);
}); });
it('getBuyQuotes()', async () => { it('getBuyQuotes()', async () => {
@ -596,13 +558,12 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync( const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync( dexOrderSampler.getBuyQuotes(
sources, sources,
expectedMakerToken, expectedMakerToken,
expectedTakerToken, expectedTakerToken,
expectedMakerFillAmounts, expectedMakerFillAmounts,
wethAddress, wethAddress,
dexOrderSampler.balancerPoolsCache,
), ),
); );
const expectedQuotes = sources.map(s => const expectedQuotes = sources.map(s =>
@ -613,7 +574,7 @@ describe('DexSampler tests', () => {
fillData: fillData:
s === ERC20BridgeSource.UniswapV2 s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] } ? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData), : {},
})), })),
); );
const uniswapV2ETHQuotes = [ const uniswapV2ETHQuotes = [
@ -644,19 +605,14 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler( const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}), new MockSamplerContract({}),
undefined, // sampler overrides undefined,
undefined, // bancor service undefined,
balancerPoolsCache, balancerPoolsCache,
); );
const [quotes] = await dexOrderSampler.executeAsync( const quotes = await dexOrderSampler.getBalancerBuyQuotesOffChainAsync(
await DexOrderSampler.ops.getBuyQuotesAsync( expectedMakerToken,
[ERC20BridgeSource.Balancer], expectedTakerToken,
expectedMakerToken, expectedMakerFillAmounts,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
); );
const expectedQuotes = pools.map(p => const expectedQuotes = pools.map(p =>
expectedMakerFillAmounts.map(a => ({ expectedMakerFillAmounts.map(a => ({
@ -689,8 +645,8 @@ describe('DexSampler tests', () => {
}); });
const dexOrderSampler = new DexOrderSampler(sampler); const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress), dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress), dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
); );
expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts);
expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts);

View File

@ -37,6 +37,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const CHAIN_ID = 1; const CHAIN_ID = 1;
const TAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress();
const MAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress();
const INTERMEDIATE_TOKEN = randomAddress();
const TRANSFORMER_DEPLOYER = randomAddress(); const TRANSFORMER_DEPLOYER = randomAddress();
const contractAddresses = { const contractAddresses = {
...getContractAddressesForChainOrThrow(CHAIN_ID), ...getContractAddressesForChainOrThrow(CHAIN_ID),
@ -124,6 +125,18 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
} as any; } as any;
} }
function getRandomTwoHopQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
const intermediateTokenAssetData = createAssetData(INTERMEDIATE_TOKEN);
return {
...getRandomQuote(side),
orders: [
{ ...getRandomOrder(), makerAssetData: intermediateTokenAssetData },
{ ...getRandomOrder(), takerAssetData: intermediateTokenAssetData },
],
isTwoHop: true,
} as any;
}
function getRandomSellQuote(): MarketSellSwapQuote { function getRandomSellQuote(): MarketSellSwapQuote {
return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote; return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote;
} }
@ -298,5 +311,52 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
}), }),
).to.eventually.be.rejectedWith('Affiliate fees denominated in sell token are not yet supported'); ).to.eventually.be.rejectedWith('Affiliate fees denominated in sell token are not yet supported');
}); });
it('Uses two `FillQuoteTransformer`s if given two-hop sell quote', async () => {
const quote = getRandomTwoHopQuote(MarketOperation.Sell) as MarketSellSwapQuote;
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { isTwoHop: true },
});
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
expect(callArgs.transformations).to.be.length(3);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
consumer.transformerNonces.fillQuoteTransformer,
);
expect(
callArgs.transformations[1].deploymentNonce.toNumber() ===
consumer.transformerNonces.fillQuoteTransformer,
);
expect(
callArgs.transformations[2].deploymentNonce.toNumber() ===
consumer.transformerNonces.payTakerTransformer,
);
const [firstHopOrder, secondHopOrder] = quote.orders;
const firstHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(firstHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAssetAmount);
expect(firstHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([firstHopOrder]));
expect(firstHopFillQuoteTransformerData.signatures).to.deep.eq([firstHopOrder.signature]);
expect(firstHopFillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(firstHopFillQuoteTransformerData.buyToken).to.eq(INTERMEDIATE_TOKEN);
const secondHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[1].data);
expect(secondHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(secondHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(contractConstants.MAX_UINT256);
expect(secondHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([secondHopOrder]));
expect(secondHopFillQuoteTransformerData.signatures).to.deep.eq([secondHopOrder.signature]);
expect(secondHopFillQuoteTransformerData.sellToken).to.eq(INTERMEDIATE_TOKEN);
expect(secondHopFillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[2].data);
expect(payTakerTransformerData.amounts).to.deep.eq([]);
expect(payTakerTransformerData.tokens).to.deep.eq([
TAKER_TOKEN,
MAKER_TOKEN,
ETH_TOKEN_ADDRESS,
INTERMEDIATE_TOKEN,
]);
});
}); });
}); });

View File

@ -17,6 +17,7 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants'; import { 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 { createFillPaths } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
@ -31,15 +32,6 @@ const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1; const CHAIN_ID = 1;
const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS }; const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS };
let originalSamplerOperations: any;
before(() => {
originalSamplerOperations = DexOrderSampler.ops;
});
after(() => {
DexOrderSampler.ops = originalSamplerOperations;
});
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder { function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return { return {
@ -153,7 +145,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
wethAddress: string, wethAddress: string,
liquidityProviderAddress?: string, liquidityProviderAddress?: string,
) => Promise<DexSample[][]>; ) => DexSample[][];
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return ( return (
@ -163,7 +155,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
_wethAddress: string, _wethAddress: string,
) => { ) => {
return Promise.resolve(sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]))); return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
}; };
} }
@ -181,7 +173,6 @@ describe('MarketOperationUtils tests', () => {
takerToken: string, takerToken: string,
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
wethAddress: string, wethAddress: string,
_balancerPoolsCache?: any,
liquidityProviderAddress?: string, liquidityProviderAddress?: string,
) => { ) => {
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress; liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
@ -206,9 +197,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[], fillAmounts: BigNumber[],
_wethAddress: string, _wethAddress: string,
) => { ) => {
return Promise.resolve( return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))),
);
}; };
} }
@ -221,12 +210,6 @@ describe('MarketOperationUtils tests', () => {
liquidityProviderAddress?: string, liquidityProviderAddress?: string,
) => BigNumber; ) => BigNumber;
type GetLiquidityProviderFromRegistryOperation = (
registryAddress: string,
takerToken: string,
makerToken: string,
) => string;
function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation {
return ( return (
_sources: ERC20BridgeSource[], _sources: ERC20BridgeSource[],
@ -239,34 +222,6 @@ describe('MarketOperationUtils tests', () => {
}; };
} }
function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation {
return (_registryAddress: string, _takerToken: string, _makerToken: string): string => {
return NULL_ADDRESS;
};
}
function getLiquidityProviderFromRegistryAndReturnCallParameters(
liquidityProviderAddress: string = NULL_ADDRESS,
): [
{ registryAddress?: string; takerToken?: string; makerToken?: string },
GetLiquidityProviderFromRegistryOperation
] {
const callArgs: { registryAddress?: string; takerToken?: string; makerToken?: string } = {
registryAddress: undefined,
takerToken: undefined,
makerToken: undefined,
};
const fn = (registryAddress: string, takerToken: string, makerToken: string): string => {
callArgs.makerToken = makerToken;
callArgs.takerToken = takerToken;
if (registryAddress !== constants.NULL_ADDRESS) {
callArgs.registryAddress = registryAddress;
}
return liquidityProviderAddress;
};
return [callArgs, fn];
}
function createDecreasingRates(count: number): BigNumber[] { function createDecreasingRates(count: number): BigNumber[] {
const rates: BigNumber[] = []; const rates: BigNumber[] = [];
const initialRate = getRandomFloat(1e-3, 1e2); const initialRate = getRandomFloat(1e-3, 1e2);
@ -317,6 +272,7 @@ describe('MarketOperationUtils tests', () => {
fromTokenIdx: 0, fromTokenIdx: 0,
toTokenIdx: 1, toTokenIdx: 1,
}, },
[ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() },
}; };
const DEFAULT_OPS = { const DEFAULT_OPS = {
@ -326,19 +282,44 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount); return orders.map(o => o.makerAssetAmount);
}, },
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRateAsync: createGetMedianSellRate(1), getMedianSellRate: createGetMedianSellRate(1),
getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(), getBalancerSellQuotesOffChainAsync: (
_makerToken: string,
_takerToken: string,
takerFillAmounts: BigNumber[],
) => [
createSamplesFromRates(
ERC20BridgeSource.Balancer,
takerFillAmounts,
createDecreasingRates(takerFillAmounts.length),
DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer],
),
],
getBalancerBuyQuotesOffChainAsync: (
_makerToken: string,
_takerToken: string,
makerFillAmounts: BigNumber[],
) => [
createSamplesFromRates(
ERC20BridgeSource.Balancer,
makerFillAmounts,
createDecreasingRates(makerFillAmounts.length).map(r => new BigNumber(1).div(r)),
DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer],
),
],
getBancorSellQuotesOffChainAsync: (_makerToken: string, _takerToken: string, takerFillAmounts: BigNumber[]) =>
createSamplesFromRates(
ERC20BridgeSource.Bancor,
takerFillAmounts,
createDecreasingRates(takerFillAmounts.length),
DEFAULT_FILL_DATA[ERC20BridgeSource.Bancor],
),
getTwoHopSellQuotes: (..._params: any[]) => [],
getTwoHopBuyQuotes: (..._params: any[]) => [],
}; };
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
DexOrderSampler.ops = {
...DEFAULT_OPS,
...ops,
} as any;
}
const MOCK_SAMPLER = ({ const MOCK_SAMPLER = ({
async executeAsync(...ops: any[]): Promise<any[]> { async executeAsync(...ops: any[]): Promise<any[]> {
return ops; return ops;
@ -346,8 +327,14 @@ describe('MarketOperationUtils tests', () => {
async executeBatchAsync(ops: any[]): Promise<any[]> { async executeBatchAsync(ops: any[]): Promise<any[]> {
return ops; return ops;
}, },
balancerPoolsCache: new BalancerPoolsCache(),
} as any) as DexOrderSampler; } as any) as DexOrderSampler;
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
Object.assign(MOCK_SAMPLER, DEFAULT_OPS);
Object.assign(MOCK_SAMPLER, ops);
}
describe('getRfqtIndicativeQuotesAsync', () => { describe('getRfqtIndicativeQuotesAsync', () => {
const partialRfqt: RfqtRequestOpts = { const partialRfqt: RfqtRequestOpts = {
apiKey: 'foo', apiKey: 'foo',
@ -434,6 +421,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Balancer, ERC20BridgeSource.Balancer,
ERC20BridgeSource.MStable, ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Bancor,
], ],
allowFallback: false, allowFallback: false,
shouldBatchBridgeOrders: false, shouldBatchBridgeOrders: false,
@ -447,9 +435,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, NUM_SAMPLES); const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0; let actualNumSamples = 0;
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length; actualNumSamples = amounts.length;
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -462,9 +450,17 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -480,7 +476,15 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES, DEFAULT_RATES,
); );
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: fn, getSellQuotes: fn,
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
args.sources = args.sources.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
}); });
const registryAddress = randomAddress(); const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils( const newMarketOperationUtils = new MarketOperationUtils(
@ -503,9 +507,17 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
}, },
}); });
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -576,7 +588,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -614,8 +626,8 @@ describe('MarketOperationUtils tests', () => {
), ),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -652,8 +664,8 @@ describe('MarketOperationUtils tests', () => {
), ),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -678,8 +690,8 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1], [ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1],
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE), getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -703,7 +715,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -724,7 +736,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01]; rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -741,7 +753,8 @@ describe('MarketOperationUtils tests', () => {
it('is able to create a order from LiquidityProvider', async () => { it('is able to create a order from LiquidityProvider', async () => {
const registryAddress = randomAddress(); const registryAddress = randomAddress();
const liquidityProviderAddress = randomAddress(); const liquidityProviderAddress = (DEFAULT_FILL_DATA[ERC20BridgeSource.LiquidityProvider] as any)
.poolAddress;
const xAsset = randomAddress(); const xAsset = randomAddress();
const yAsset = randomAddress(); const yAsset = randomAddress();
const toSell = fromTokenUnitAmount(10); const toSell = fromTokenUnitAmount(10);
@ -752,14 +765,10 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.LiquidityProvider]: createDecreasingRates(5), [ERC20BridgeSource.LiquidityProvider]: createDecreasingRates(5),
}, },
); );
const [
getLiquidityProviderParams,
getLiquidityProviderFn,
] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress);
replaceSamplerOps({ replaceSamplerOps({
getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT], getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT],
getSellQuotesAsync: getSellQuotesFn, getSellQuotes: getSellQuotesFn,
getLiquidityProviderFromRegistry: getLiquidityProviderFn,
}); });
const sampler = new MarketOperationUtils( const sampler = new MarketOperationUtils(
@ -776,7 +785,12 @@ describe('MarketOperationUtils tests', () => {
}), }),
], ],
Web3Wrapper.toBaseUnitAmount(10, 18), Web3Wrapper.toBaseUnitAmount(10, 18),
{ excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false }, {
excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor),
numSamples: 4,
bridgeSlippage: 0,
shouldBatchBridgeOrders: false,
},
); );
const result = ordersAndReport.optimizedOrders; const result = ordersAndReport.optimizedOrders;
expect(result.length).to.eql(1); expect(result.length).to.eql(1);
@ -791,9 +805,6 @@ describe('MarketOperationUtils tests', () => {
expect(result[0].takerAssetAmount).to.bignumber.eql(toSell); expect(result[0].takerAssetAmount).to.bignumber.eql(toSell);
expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider); expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider);
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
}); });
it('batches contiguous bridge sources', async () => { it('batches contiguous bridge sources', async () => {
@ -803,7 +814,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -860,9 +871,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, 16); const numSamples = _.random(1, 16);
let actualNumSamples = 0; let actualNumSamples = 0;
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length; actualNumSamples = amounts.length;
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -875,9 +886,17 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => { it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -893,7 +912,15 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES, DEFAULT_RATES,
); );
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: fn, getBuyQuotes: fn,
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
args.sources = args.sources.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
}); });
const registryAddress = randomAddress(); const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils( const newMarketOperationUtils = new MarketOperationUtils(
@ -916,9 +943,17 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length)); const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = []; let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => { getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice()); sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress); return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
}, },
}); });
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, { await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -988,7 +1023,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1026,8 +1061,8 @@ describe('MarketOperationUtils tests', () => {
), ),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1063,8 +1098,8 @@ describe('MarketOperationUtils tests', () => {
), ),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE), getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1087,7 +1122,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1107,7 +1142,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [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.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1128,7 +1163,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01];
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01];
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
}); });
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),

View File

@ -12,15 +12,17 @@ import {
CollapsedFill, CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill, NativeCollapsedFill,
} from '../src/utils/market_operation_utils/types'; } from '../src/utils/market_operation_utils/types';
import { QuoteRequestor } from '../src/utils/quote_requestor'; import { QuoteRequestor } from '../src/utils/quote_requestor';
import { import {
BridgeReportSource, BridgeReportSource,
generateQuoteReport,
MultiHopReportSource,
NativeOrderbookReportSource, NativeOrderbookReportSource,
NativeRFQTReportSource, NativeRFQTReportSource,
QuoteReportGenerator,
QuoteReportSource, QuoteReportSource,
} from './../src/utils/quote_report_generator'; } from './../src/utils/quote_report_generator';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
@ -47,309 +49,378 @@ const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill =
}; };
}; };
describe('QuoteReportGenerator', async () => { describe('generateQuoteReport', async () => {
describe('generateReport', async () => { it('should generate report properly for sell', () => {
it('should generate report properly for sell', () => { const marketOperation: MarketOperation = MarketOperation.Sell;
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = { const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber, source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000), input: new BigNumber(10000),
output: new BigNumber(10001), output: new BigNumber(10001),
}; };
const kyberSample2: DexSample = { const kyberSample2: DexSample = {
source: ERC20BridgeSource.Kyber, source: ERC20BridgeSource.Kyber,
input: new BigNumber(10003), input: new BigNumber(10003),
output: new BigNumber(10004), output: new BigNumber(10004),
}; };
const uniswapSample1: DexSample = { const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2, source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003), input: new BigNumber(10003),
output: new BigNumber(10004), output: new BigNumber(10004),
}; };
const uniswapSample2: DexSample = { const uniswapSample2: DexSample = {
source: ERC20BridgeSource.UniswapV2, source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10005), input: new BigNumber(10005),
output: new BigNumber(10006), output: new BigNumber(10006),
}; };
const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2]; const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2];
const orderbookOrder1FillableAmount = new BigNumber(1000); const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1', signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount, takerAssetAmount: orderbookOrder1FillableAmount,
});
const orderbookOrder2FillableAmount = new BigNumber(99);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(99),
});
const rfqtOrder1FillableAmount = new BigNumber(100);
const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder1',
takerAssetAmount: rfqtOrder1FillableAmount,
});
const rfqtOrder2FillableAmount = new BigNumber(1001);
const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder2',
takerAssetAmount: rfqtOrder2FillableAmount.plus(100),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [
orderbookOrder1FillableAmount,
rfqtOrder1FillableAmount,
rfqtOrder2FillableAmount,
orderbookOrder2FillableAmount,
];
// generate path
const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() };
const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() };
const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2);
const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2);
const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill];
// quote generator mock
const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(orderbookOrder2)))
.returns(() => {
return undefined;
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder1)))
.returns(() => {
return 'https://rfqt1.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder2)))
.returns(() => {
return 'https://rfqt2.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
const orderReport = new QuoteReportGenerator(
marketOperation,
dexQuotes,
nativeOrders,
orderFillableAmounts,
pathGenerated,
quoteRequestor.object,
).generateReport();
const rfqtOrder1Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder1.makerAssetAmount,
takerAmount: rfqtOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(rfqtOrder1),
nativeOrder: rfqtOrder1,
fillableTakerAmount: rfqtOrder1FillableAmount,
isRfqt: true,
makerUri: 'https://rfqt1.provider.club',
};
const rfqtOrder2Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder2.makerAssetAmount,
takerAmount: rfqtOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(rfqtOrder2),
nativeOrder: rfqtOrder2,
fillableTakerAmount: rfqtOrder2FillableAmount,
isRfqt: true,
makerUri: 'https://rfqt2.provider.club',
};
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2),
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
isRfqt: false,
};
const uniswap1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.output,
takerAmount: uniswapSample1.input,
};
const uniswap2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample2.output,
takerAmount: uniswapSample2.input,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
};
const kyber2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample2.output,
takerAmount: kyberSample2.input,
};
const expectedSourcesConsidered: QuoteReportSource[] = [
kyber1Source,
kyber2Source,
uniswap1Source,
uniswap2Source,
orderbookOrder1Source,
rfqtOrder1Source,
rfqtOrder2Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [
rfqtOrder2Source,
orderbookOrder2Source,
uniswap2Source,
kyber2Source,
];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(
expectedSourceDelivered,
`sourceDelivered incorrect at index ${idx}`,
);
});
quoteRequestor.verifyAll();
}); });
it('should handle properly for buy without quoteRequestor', () => { const orderbookOrder2FillableAmount = new BigNumber(99);
const marketOperation: MarketOperation = MarketOperation.Buy; const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
const kyberSample1: DexSample = { signature: 'orderbookOrder2',
source: ERC20BridgeSource.Kyber, takerAssetAmount: orderbookOrder2FillableAmount.plus(99),
input: new BigNumber(10000), });
output: new BigNumber(10001), const rfqtOrder1FillableAmount = new BigNumber(100);
}; const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({
const uniswapSample1: DexSample = { signature: 'rfqtOrder1',
source: ERC20BridgeSource.UniswapV2, takerAssetAmount: rfqtOrder1FillableAmount,
input: new BigNumber(10003), });
output: new BigNumber(10004), const rfqtOrder2FillableAmount = new BigNumber(1001);
}; const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1]; signature: 'rfqtOrder2',
takerAssetAmount: rfqtOrder2FillableAmount.plus(100),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [
orderbookOrder1FillableAmount,
rfqtOrder1FillableAmount,
rfqtOrder2FillableAmount,
orderbookOrder2FillableAmount,
];
const orderbookOrder1FillableAmount = new BigNumber(1000); // generate path
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({ const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() };
signature: 'orderbookOrder1', const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() };
takerAssetAmount: orderbookOrder1FillableAmount.plus(101), const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2);
}); const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2);
const orderbookOrder2FillableAmount = new BigNumber(5000); const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill];
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(101),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount];
// generate path // quote generator mock
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() }; quoteRequestor
const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() }; .setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(orderbookOrder2)))
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill]; .returns(() => {
return undefined;
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder1)))
.returns(() => {
return 'https://rfqt1.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderHash(orderHashUtils.getOrderHash(rfqtOrder2)))
.returns(() => {
return 'https://rfqt2.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
const orderReport = new QuoteReportGenerator( const orderReport = generateQuoteReport(
marketOperation, marketOperation,
dexQuotes, dexQuotes,
nativeOrders, [],
orderFillableAmounts, nativeOrders,
pathGenerated, orderFillableAmounts,
).generateReport(); pathGenerated,
quoteRequestor.object,
);
const orderbookOrder1Source: NativeOrderbookReportSource = { const rfqtOrder1Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native, liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount, makerAmount: rfqtOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount, takerAmount: rfqtOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1), orderHash: orderHashUtils.getOrderHash(rfqtOrder1),
nativeOrder: orderbookOrder1, nativeOrder: rfqtOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount, fillableTakerAmount: rfqtOrder1FillableAmount,
isRfqt: false, isRfqt: true,
}; makerUri: 'https://rfqt1.provider.club',
const orderbookOrder2Source: NativeOrderbookReportSource = { };
liquiditySource: ERC20BridgeSource.Native, const rfqtOrder2Source: NativeRFQTReportSource = {
makerAmount: orderbookOrder2.makerAssetAmount, liquiditySource: ERC20BridgeSource.Native,
takerAmount: orderbookOrder2.takerAssetAmount, makerAmount: rfqtOrder2.makerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2), takerAmount: rfqtOrder2.takerAssetAmount,
nativeOrder: orderbookOrder2, orderHash: orderHashUtils.getOrderHash(rfqtOrder2),
fillableTakerAmount: orderbookOrder2FillableAmount, nativeOrder: rfqtOrder2,
isRfqt: false, fillableTakerAmount: rfqtOrder2FillableAmount,
}; isRfqt: true,
const uniswap1Source: BridgeReportSource = { makerUri: 'https://rfqt2.provider.club',
liquiditySource: ERC20BridgeSource.UniswapV2, };
makerAmount: uniswapSample1.input, const orderbookOrder1Source: NativeOrderbookReportSource = {
takerAmount: uniswapSample1.output, liquiditySource: ERC20BridgeSource.Native,
}; makerAmount: orderbookOrder1.makerAssetAmount,
const kyber1Source: BridgeReportSource = { takerAmount: orderbookOrder1.takerAssetAmount,
liquiditySource: ERC20BridgeSource.Kyber, orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
makerAmount: kyberSample1.input, nativeOrder: orderbookOrder1,
takerAmount: kyberSample1.output, fillableTakerAmount: orderbookOrder1FillableAmount,
}; isRfqt: false,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2),
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
isRfqt: false,
};
const uniswap1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.output,
takerAmount: uniswapSample1.input,
};
const uniswap2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample2.output,
takerAmount: uniswapSample2.input,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
};
const kyber2Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample2.output,
takerAmount: kyberSample2.input,
};
const expectedSourcesConsidered: QuoteReportSource[] = [ const expectedSourcesConsidered: QuoteReportSource[] = [
kyber1Source, kyber1Source,
uniswap1Source, kyber2Source,
orderbookOrder1Source, uniswap1Source,
orderbookOrder2Source, uniswap2Source,
]; orderbookOrder1Source,
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length); rfqtOrder1Source,
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => { rfqtOrder2Source,
const expectedSourceConsidered = expectedSourcesConsidered[idx]; orderbookOrder2Source,
expect(actualSourcesConsidered).to.eql( ];
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source]; expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) { const expectedSourceConsidered = expectedSourcesConsidered[idx];
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [ expect(actualSourcesConsidered).to.eql(
'fillableMakerAssetAmount', expectedSourceConsidered,
'fillableTakerAssetAmount', `sourceConsidered incorrect at index ${idx}`,
'fillableTakerFeeAmount', );
]) as SignedOrder; });
}
expect(actualSourceDelivered).to.eql( const expectedSourcesDelivered: QuoteReportSource[] = [
expectedSourceDelivered, rfqtOrder2Source,
`sourceDelivered incorrect at index ${idx}`, orderbookOrder2Source,
); uniswap2Source,
}); kyber2Source,
];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
});
quoteRequestor.verifyAll();
});
it('should handle properly for buy without quoteRequestor', () => {
const marketOperation: MarketOperation = MarketOperation.Buy;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
};
const uniswapSample1: DexSample = {
source: ERC20BridgeSource.UniswapV2,
input: new BigNumber(10003),
output: new BigNumber(10004),
};
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1];
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const orderbookOrder2FillableAmount = new BigNumber(5000);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(101),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount];
// generate path
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1);
const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() };
const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() };
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill];
const orderReport = generateQuoteReport(
marketOperation,
dexQuotes,
[],
nativeOrders,
orderFillableAmounts,
pathGenerated,
);
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder2),
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
isRfqt: false,
};
const uniswap1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.input,
takerAmount: uniswapSample1.output,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.input,
takerAmount: kyberSample1.output,
};
const expectedSourcesConsidered: QuoteReportSource[] = [
kyber1Source,
uniswap1Source,
orderbookOrder1Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
}); });
}); });
it('should correctly generate report for a two-hop quote', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
};
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const twoHopSample: DexSample<MultiHopFillData> = {
source: ERC20BridgeSource.MultiHop,
input: new BigNumber(3005),
output: new BigNumber(3006),
fillData: {
intermediateToken: hexUtils.random(20),
firstHopSource: {
source: ERC20BridgeSource.Balancer,
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
},
secondHopSource: {
source: ERC20BridgeSource.Curve,
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
},
},
};
const orderReport = generateQuoteReport(
marketOperation,
[kyberSample1],
[twoHopSample],
[orderbookOrder1],
[orderbookOrder1FillableAmount],
twoHopSample,
);
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
};
const twoHopSource: MultiHopReportSource = {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: twoHopSample.output,
takerAmount: twoHopSample.input,
hopSources: [ERC20BridgeSource.Balancer, ERC20BridgeSource.Curve],
};
const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
expect(orderReport.sourcesDelivered.length).to.eql(1);
expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource);
});
}); });

View File

@ -29,7 +29,7 @@ export type SampleSellsLPHandler = (
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
takerTokenAmounts: BigNumber[], takerTokenAmounts: BigNumber[],
) => SampleResults; ) => [SampleResults, string];
export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults; export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults;
export type SampleSellsMBHandler = ( export type SampleSellsMBHandler = (
multiBridgeAddress: string, multiBridgeAddress: string,
@ -40,7 +40,7 @@ export type SampleSellsMBHandler = (
) => SampleResults; ) => SampleResults;
const DUMMY_PROVIDER = { const DUMMY_PROVIDER = {
sendAsync: (...args: any[]): any => { sendAsync: (..._args: any[]): any => {
/* no-op */ /* no-op */
}, },
}; };
@ -73,7 +73,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
public batchCall(callDatas: string[]): ContractFunctionObj<string[]> { public batchCall(callDatas: string[]): ContractFunctionObj<string[]> {
return { return {
...super.batchCall(callDatas), ...super.batchCall(callDatas),
callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)), callAsync: async (..._callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
}; };
} }
@ -107,7 +107,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
takerAssetAmounts: BigNumber[], takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromKyberNetwork, super.sampleSellsFromKyberNetwork,
this._handlers.sampleSellsFromKyberNetwork, this._handlers.sampleSellsFromKyberNetwork,
@ -121,7 +121,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
takerAssetAmounts: BigNumber[], takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromEth2Dai, super.sampleSellsFromEth2Dai,
this._handlers.sampleSellsFromEth2Dai, this._handlers.sampleSellsFromEth2Dai,
@ -135,7 +135,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
takerAssetAmounts: BigNumber[], takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromUniswap, super.sampleSellsFromUniswap,
this._handlers.sampleSellsFromUniswap, this._handlers.sampleSellsFromUniswap,
@ -145,10 +145,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
); );
} }
public sampleSellsFromUniswapV2( public sampleSellsFromUniswapV2(path: string[], takerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
path: string[],
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromUniswapV2, super.sampleSellsFromUniswapV2,
this._handlers.sampleSellsFromUniswapV2, this._handlers.sampleSellsFromUniswapV2,
@ -162,7 +159,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
takerAssetAmounts: BigNumber[], takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<[BigNumber[], string]> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromLiquidityProviderRegistry, super.sampleSellsFromLiquidityProviderRegistry,
this._handlers.sampleSellsFromLiquidityProviderRegistry, this._handlers.sampleSellsFromLiquidityProviderRegistry,
@ -179,7 +176,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
intermediateToken: string, intermediateToken: string,
makerToken: string, makerToken: string,
takerAssetAmounts: BigNumber[], takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleSellsFromMultiBridge, super.sampleSellsFromMultiBridge,
this._handlers.sampleSellsFromMultiBridge, this._handlers.sampleSellsFromMultiBridge,
@ -195,7 +192,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
makerAssetAmounts: BigNumber[], makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleBuysFromEth2Dai, super.sampleBuysFromEth2Dai,
this._handlers.sampleBuysFromEth2Dai, this._handlers.sampleBuysFromEth2Dai,
@ -209,7 +206,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string, takerToken: string,
makerToken: string, makerToken: string,
makerAssetAmounts: BigNumber[], makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> { ): ContractFunctionObj<BigNumber[]> {
return this._wrapCall( return this._wrapCall(
super.sampleBuysFromUniswap, super.sampleBuysFromUniswap,
this._handlers.sampleBuysFromUniswap, this._handlers.sampleBuysFromUniswap,
@ -219,10 +216,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
); );
} }
public sampleBuysFromUniswapV2( public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
path: string[],
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall( return this._wrapCall(
super.sampleBuysFromUniswapV2, super.sampleBuysFromUniswapV2,
this._handlers.sampleBuysFromUniswapV2, this._handlers.sampleBuysFromUniswapV2,
@ -241,7 +235,12 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
if (handler && this.getSelector(name) === selector) { if (handler && this.getSelector(name) === selector) {
const args = this.getABIDecodedTransactionData<any>(name, callData); const args = this.getABIDecodedTransactionData<any>(name, callData);
const result = (handler as any)(...args); const result = (handler as any)(...args);
return this._lookupAbiEncoder(this.getFunctionSignature(name)).encodeReturnValues([result]); const encoder = this._lookupAbiEncoder(this.getFunctionSignature(name));
if (encoder.getReturnValueDataItem().components!.length === 1) {
return encoder.encodeReturnValues([result]);
} else {
return encoder.encodeReturnValues(result);
}
} }
} }
if (selector === this.getSelector('batchCall')) { if (selector === this.getSelector('batchCall')) {
@ -260,7 +259,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
): ContractFunctionObj<TResult> { ): ContractFunctionObj<TResult> {
return { return {
...superFn.call(this, ...args), ...superFn.call(this, ...args),
callAsync: async (...callArgs: any[]): Promise<TResult> => { callAsync: async (..._callArgs: any[]): Promise<TResult> => {
if (!handler) { if (!handler) {
throw new Error(`${superFn.name} handler undefined`); throw new Error(`${superFn.name} handler undefined`);
} }

View File

@ -39,6 +39,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
bestCaseQuoteInfo: quoteInfo, bestCaseQuoteInfo: quoteInfo,
worstCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo,
sourceBreakdown: breakdown, sourceBreakdown: breakdown,
isTwoHop: false,
}; };
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {

View File

@ -4,11 +4,13 @@
* ----------------------------------------------------------------------------- * -----------------------------------------------------------------------------
*/ */
export * from '../test/generated-wrappers/approximate_buys'; export * from '../test/generated-wrappers/approximate_buys';
export * from '../test/generated-wrappers/balancer_sampler';
export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/curve_sampler';
export * from '../test/generated-wrappers/dummy_liquidity_provider'; export * from '../test/generated-wrappers/dummy_liquidity_provider';
export * from '../test/generated-wrappers/dummy_liquidity_provider_registry'; export * from '../test/generated-wrappers/dummy_liquidity_provider_registry';
export * from '../test/generated-wrappers/erc20_bridge_sampler'; export * from '../test/generated-wrappers/erc20_bridge_sampler';
export * from '../test/generated-wrappers/eth2_dai_sampler'; export * from '../test/generated-wrappers/eth2_dai_sampler';
export * from '../test/generated-wrappers/i_balancer';
export * from '../test/generated-wrappers/i_curve'; export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_eth2_dai'; export * from '../test/generated-wrappers/i_eth2_dai';
export * from '../test/generated-wrappers/i_kyber_hint_handler'; export * from '../test/generated-wrappers/i_kyber_hint_handler';
@ -31,5 +33,6 @@ export * from '../test/generated-wrappers/native_order_sampler';
export * from '../test/generated-wrappers/sampler_utils'; export * from '../test/generated-wrappers/sampler_utils';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler';
export * from '../test/generated-wrappers/test_native_order_sampler'; export * from '../test/generated-wrappers/test_native_order_sampler';
export * from '../test/generated-wrappers/two_hop_sampler';
export * from '../test/generated-wrappers/uniswap_sampler'; export * from '../test/generated-wrappers/uniswap_sampler';
export * from '../test/generated-wrappers/uniswap_v2_sampler'; export * from '../test/generated-wrappers/uniswap_v2_sampler';

View File

@ -9,11 +9,13 @@
"generated-artifacts/ILiquidityProvider.json", "generated-artifacts/ILiquidityProvider.json",
"generated-artifacts/ILiquidityProviderRegistry.json", "generated-artifacts/ILiquidityProviderRegistry.json",
"test/generated-artifacts/ApproximateBuys.json", "test/generated-artifacts/ApproximateBuys.json",
"test/generated-artifacts/BalancerSampler.json",
"test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/CurveSampler.json",
"test/generated-artifacts/DummyLiquidityProvider.json", "test/generated-artifacts/DummyLiquidityProvider.json",
"test/generated-artifacts/DummyLiquidityProviderRegistry.json", "test/generated-artifacts/DummyLiquidityProviderRegistry.json",
"test/generated-artifacts/ERC20BridgeSampler.json", "test/generated-artifacts/ERC20BridgeSampler.json",
"test/generated-artifacts/Eth2DaiSampler.json", "test/generated-artifacts/Eth2DaiSampler.json",
"test/generated-artifacts/IBalancer.json",
"test/generated-artifacts/ICurve.json", "test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IEth2Dai.json", "test/generated-artifacts/IEth2Dai.json",
"test/generated-artifacts/IKyberHintHandler.json", "test/generated-artifacts/IKyberHintHandler.json",
@ -36,6 +38,7 @@
"test/generated-artifacts/SamplerUtils.json", "test/generated-artifacts/SamplerUtils.json",
"test/generated-artifacts/TestERC20BridgeSampler.json", "test/generated-artifacts/TestERC20BridgeSampler.json",
"test/generated-artifacts/TestNativeOrderSampler.json", "test/generated-artifacts/TestNativeOrderSampler.json",
"test/generated-artifacts/TwoHopSampler.json",
"test/generated-artifacts/UniswapSampler.json", "test/generated-artifacts/UniswapSampler.json",
"test/generated-artifacts/UniswapV2Sampler.json" "test/generated-artifacts/UniswapV2Sampler.json"
] ]

View File

@ -9,6 +9,10 @@
{ {
"note": "Update `ERC20BridgeSampler` artifact", "note": "Update `ERC20BridgeSampler` artifact",
"pr": 2633 "pr": 2633
},
{
"note": "Remove `ERC20BridgeSampler` artifact",
"pr": 2647
} }
] ]
}, },

View File

@ -1,475 +0,0 @@
{
"schemaVersion": "2.0.0",
"contractName": "IERC20BridgeSampler",
"compilerOutput": {
"abi": [
{
"constant": true,
"inputs": [{ "internalType": "bytes[]", "name": "callDatas", "type": "bytes[]" }],
"name": "batchCall",
"outputs": [{ "internalType": "bytes[]", "name": "callResults", "type": "bytes[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" }
],
"name": "getLiquidityProviderFromRegistry",
"outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[]",
"name": "orders",
"type": "tuple[]"
},
{ "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" },
{ "internalType": "address", "name": "devUtilsAddress", "type": "address" }
],
"name": "getOrderFillableMakerAssetAmounts",
"outputs": [
{ "internalType": "uint256[]", "name": "orderFillableMakerAssetAmounts", "type": "uint256[]" }
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[]",
"name": "orders",
"type": "tuple[]"
},
{ "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" },
{ "internalType": "address", "name": "devUtilsAddress", "type": "address" }
],
"name": "getOrderFillableTakerAssetAmounts",
"outputs": [
{ "internalType": "uint256[]", "name": "orderFillableTakerAssetAmounts", "type": "uint256[]" }
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "curveAddress", "type": "address" },
{ "internalType": "int128", "name": "fromTokenIdx", "type": "int128" },
{ "internalType": "int128", "name": "toTokenIdx", "type": "int128" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromCurve",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromEth2Dai",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" },
{
"components": [
{ "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" },
{ "internalType": "uint256", "name": "maxIterations", "type": "uint256" }
],
"internalType": "struct IERC20BridgeSampler.FakeBuyOptions",
"name": "opts",
"type": "tuple"
}
],
"name": "sampleBuysFromKyberNetwork",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" },
{
"components": [
{ "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" },
{ "internalType": "uint256", "name": "maxIterations", "type": "uint256" }
],
"internalType": "struct IERC20BridgeSampler.FakeBuyOptions",
"name": "opts",
"type": "tuple"
}
],
"name": "sampleBuysFromLiquidityProviderRegistry",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromUniswap",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address[]", "name": "path", "type": "address[]" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromUniswapV2",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "curveAddress", "type": "address" },
{ "internalType": "int128", "name": "fromTokenIdx", "type": "int128" },
{ "internalType": "int128", "name": "toTokenIdx", "type": "int128" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromCurve",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromEth2Dai",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromKyberNetwork",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromLiquidityProviderRegistry",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "multibridge", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "intermediateToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromMultiBridge",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromUniswap",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address[]", "name": "path", "type": "address[]" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromUniswapV2",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
],
"devdoc": {
"methods": {
"batchCall(bytes[])": {
"details": "Call multiple public functions on this contract in a single transaction.",
"params": { "callDatas": "ABI-encoded call data for each function call." },
"return": "callResults ABI-encoded results data for each call."
},
"getLiquidityProviderFromRegistry(address,address,address)": {
"details": "Returns the address of a liquidity provider for the given market (takerToken, makerToken), from a registry of liquidity providers. Returns address(0) if no such provider exists in the registry.",
"params": {
"makerToken": "Maker asset managed by liquidity provider.",
"takerToken": "Taker asset managed by liquidity provider."
},
"return": "providerAddress Address of the liquidity provider."
},
"getOrderFillableMakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": {
"details": "Queries the fillable maker asset amounts of native orders.",
"params": {
"devUtilsAddress": "Address to the DevUtils contract.",
"orderSignatures": "Signatures for each respective order in `orders`.",
"orders": "Native orders to query."
},
"return": "orderFillableMakerAssetAmounts How much maker asset can be filled by each order in `orders`."
},
"getOrderFillableTakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": {
"details": "Queries the fillable taker asset amounts of native orders.",
"params": {
"devUtilsAddress": "Address to the DevUtils contract.",
"orderSignatures": "Signatures for each respective order in `orders`.",
"orders": "Native orders to query."
},
"return": "orderFillableTakerAssetAmounts How much taker asset can be filled by each order in `orders`."
},
"sampleBuysFromCurve(address,int128,int128,uint256[])": {
"details": "Sample buy quotes from Curve.",
"params": {
"curveAddress": "Address of the Curve contract.",
"fromTokenIdx": "Index of the taker token (what to sell).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"toTokenIdx": "Index of the maker token (what to buy)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromEth2Dai(address,address,uint256[])": {
"details": "Sample buy quotes from Eth2Dai/Oasis.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromKyberNetwork(address,address,uint256[],(uint256,uint256))": {
"details": "Sample buy quotes from Kyber.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"opts": "`FakeBuyOptions` specifying target slippage and max iterations.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromLiquidityProviderRegistry(address,address,address,uint256[],(uint256,uint256))": {
"details": "Sample buy quotes from an arbitrary on-chain liquidity provider.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"opts": "`FakeBuyOptions` specifying target slippage and max iterations.",
"registryAddress": "Address of the liquidity provider registry contract.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromUniswap(address,address,uint256[])": {
"details": "Sample buy quotes from Uniswap.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromUniswapV2(address[],uint256[])": {
"details": "Sample buy quotes from UniswapV2.",
"params": {
"makerTokenAmounts": "Maker token buy amount for each sample.",
"path": "Token route."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleSellsFromCurve(address,int128,int128,uint256[])": {
"details": "Sample sell quotes from Curve.",
"params": {
"curveAddress": "Address of the Curve contract.",
"fromTokenIdx": "Index of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample.",
"toTokenIdx": "Index of the maker token (what to buy)."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromEth2Dai(address,address,uint256[])": {
"details": "Sample sell quotes from Eth2Dai/Oasis.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromKyberNetwork(address,address,uint256[])": {
"details": "Sample sell quotes from Kyber.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromLiquidityProviderRegistry(address,address,address,uint256[])": {
"details": "Sample sell quotes from an arbitrary on-chain liquidity provider.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"registryAddress": "Address of the liquidity provider registry contract.",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromMultiBridge(address,address,address,address,uint256[])": {
"details": "Sample sell quotes from MultiBridge.",
"params": {
"intermediateToken": "The address of the intermediate token to use in an indirect route.",
"makerToken": "Address of the maker token (what to buy).",
"multibridge": "Address of the MultiBridge contract.",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromUniswap(address,address,uint256[])": {
"details": "Sample sell quotes from Uniswap.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromUniswapV2(address[],uint256[])": {
"details": "Sample sell quotes from UniswapV2.",
"params": {
"path": "Token route.",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
}
}
},
"evm": { "bytecode": { "object": "0x" }, "deployedBytecode": { "object": "0x" } }
},
"compiler": {
"name": "solc",
"version": "0.5.17+commit.d19bba13",
"settings": {
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true }
},
"outputSelection": {
"*": {
"*": [
"abi",
"devdoc",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
},
"evmVersion": "istanbul"
}
},
"chains": {}
}

View File

@ -32,7 +32,7 @@
"wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers" "wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers"
}, },
"config": { "config": {
"abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json" "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json"
}, },
"gitpkg": { "gitpkg": {
"registry": "git@github.com:0xProject/gitpkg-registry.git" "registry": "git@github.com:0xProject/gitpkg-registry.git"

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es6",
"lib": ["es2017", "dom", "esnext.asynciterable", "es2018.promise"], "lib": ["es2017", "dom", "esnext.asynciterable", "es2018.promise"],
"experimentalDecorators": true, "experimentalDecorators": true,
"downlevelIteration": true, "downlevelIteration": true,