Merge branch 'development' into feat/asset-swapper/rfqt-indicative-without-takerAddress

This commit is contained in:
F. Eugene Aumson
2020-08-27 13:49:46 -04:00
79 changed files with 3128 additions and 2438 deletions

View File

@@ -451,8 +451,8 @@ workflows:
- test-python:
requires:
- build
- static-tests-python:
requires:
- build
#- static-tests-python:
# requires:
# - build
# skip python tox run for now, as we don't yet have multiple test environments to support.
# - test-rest-python

View File

@@ -21,6 +21,10 @@
{
"note": "Added `MooniswapBridge`",
"pr": 2675
},
{
"note": "Reworked `KyberBridge`",
"pr": 2683
}
]
},

View File

@@ -1,6 +1,6 @@
/*
Copyright 2019 ZeroEx Intl.
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -45,6 +45,7 @@ contract KyberBridge is
uint256 fromTokenBalance;
uint256 payableAmount;
uint256 conversionRate;
bytes hint;
}
/// @dev Kyber ETH pseudo-address.
@@ -85,47 +86,35 @@ contract KyberBridge is
state.kyber = IKyberNetworkProxy(_getKyberNetworkProxyAddress());
state.weth = IEtherToken(_getWethAddress());
// Decode the bridge data to get the `fromTokenAddress`.
(state.fromTokenAddress) = abi.decode(bridgeData, (address));
(state.fromTokenAddress, state.hint) = abi.decode(bridgeData, (address, bytes));
// Query the balance of "from" tokens.
state.fromTokenBalance = IERC20Token(state.fromTokenAddress).balanceOf(address(this));
if (state.fromTokenBalance == 0) {
// Return failure if no input tokens.
return BRIDGE_FAILED;
}
// Compute the conversion rate, expressed in 18 decimals.
// The sequential notation is to get around stack limits.
state.conversionRate = KYBER_RATE_BASE;
state.conversionRate = state.conversionRate.safeMul(amount);
state.conversionRate = state.conversionRate.safeMul(
10 ** uint256(LibERC20Token.decimals(state.fromTokenAddress))
);
state.conversionRate = state.conversionRate.safeDiv(state.fromTokenBalance);
state.conversionRate = state.conversionRate.safeDiv(
10 ** uint256(LibERC20Token.decimals(toTokenAddress))
);
if (state.fromTokenAddress == toTokenAddress) {
// Just transfer the tokens if they're the same.
LibERC20Token.transfer(state.fromTokenAddress, to, state.fromTokenBalance);
return BRIDGE_SUCCESS;
} else if (state.fromTokenAddress != address(state.weth)) {
// If the input token is not WETH, grant an allowance to the exchange
// to spend them.
}
if (state.fromTokenAddress == address(state.weth)) {
// From WETH
state.fromTokenAddress = KYBER_ETH_ADDRESS;
state.payableAmount = state.fromTokenBalance;
state.weth.withdraw(state.fromTokenBalance);
} else {
LibERC20Token.approveIfBelow(
state.fromTokenAddress,
address(state.kyber),
state.fromTokenBalance
);
} else {
// If the input token is WETH, unwrap it and attach it to the call.
state.fromTokenAddress = KYBER_ETH_ADDRESS;
state.payableAmount = state.fromTokenBalance;
state.weth.withdraw(state.fromTokenBalance);
}
bool isToTokenWeth = toTokenAddress == address(state.weth);
// Try to sell all of this contract's input token balance through
// `KyberNetworkProxy.trade()`.
uint256 boughtAmount = state.kyber.trade.value(state.payableAmount)(
uint256 boughtAmount = state.kyber.tradeWithHint.value(state.payableAmount)(
// Input token.
state.fromTokenAddress,
// Sell amount.
@@ -137,11 +126,11 @@ contract KyberBridge is
isToTokenWeth ? address(uint160(address(this))) : address(uint160(to)),
// Buy as much as possible.
uint256(-1),
// Compute the minimum conversion rate, which is expressed in units with
// 18 decimal places.
state.conversionRate,
// The minimum conversion rate
1,
// No affiliate address.
address(0)
address(0),
state.hint
);
// Wrap ETH output and transfer to recipient.
if (isToTokenWeth) {
@@ -173,4 +162,5 @@ contract KyberBridge is
{
return LEGACY_WALLET_MAGIC_VALUE;
}
}

View File

@@ -42,5 +42,31 @@ interface IKyberNetworkProxy {
)
external
payable
returns(uint256 boughtAmount);
returns (uint256 boughtAmount);
/// @dev Sells `sellTokenAddress` tokens for `buyTokenAddress` tokens
/// using a hint for the reserve.
/// @param sellTokenAddress Token to sell.
/// @param sellAmount Amount of tokens to sell.
/// @param buyTokenAddress Token to buy.
/// @param recipientAddress Address to send bought tokens to.
/// @param maxBuyTokenAmount A limit on the amount of tokens to buy.
/// @param minConversionRate The minimal conversion rate. If actual rate
/// is lower, trade is canceled.
/// @param walletId The wallet ID to send part of the fees
/// @param hint The hint for the selective inclusion (or exclusion) of reserves
/// @return boughtAmount Amount of tokens bought.
function tradeWithHint(
address sellTokenAddress,
uint256 sellAmount,
address buyTokenAddress,
address payable recipientAddress,
uint256 maxBuyTokenAmount,
uint256 minConversionRate,
address payable walletId,
bytes calldata hint
)
external
payable
returns (uint256 boughtAmount);
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
Copyright 2019 ZeroEx Intl.
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@ contract DeploymentConstants {
address constant private WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
/// @dev Mainnet address of the KyberNetworkProxy contract.
address constant private KYBER_NETWORK_PROXY_ADDRESS = 0x9AAb3f75489902f3a48495025729a0AF77d4b11e;
/// @dev Mainnet address of the KyberHintHandler contract.
address constant private KYBER_HINT_HANDLER_ADDRESS = 0xa1C0Fa73c39CFBcC11ec9Eb1Afc665aba9996E2C;
/// @dev Mainnet address of the `UniswapExchangeFactory` contract.
address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
/// @dev Mainnet address of the `UniswapV2Router01` contract.
@@ -155,6 +157,16 @@ contract DeploymentConstants {
return KYBER_NETWORK_PROXY_ADDRESS;
}
/// @dev Overridable way to get the `KyberHintHandler` address.
/// @return kyberAddress The `IKyberHintHandler` address.
function _getKyberHintHandlerAddress()
internal
view
returns (address hintHandlerAddress)
{
return KYBER_HINT_HANDLER_ADDRESS;
}
/// @dev Overridable way to get the WETH address.
/// @return wethAddress The WETH address.
function _getWethAddress()

View File

@@ -22,7 +22,7 @@
"test_cli:clean": "rm -rf test-cli/test_typescript/lib",
"test_cli:build": "tsc --project test-cli/tsconfig.json",
"test_cli:test_typescript": "mocha --require source-map-support/register --require make-promises-safe test-cli/test_typescript/lib/**/*_test.js --timeout 100000 --bail --exit",
"test_cli:lint": "run-p test_cli:lint_solidity test_cli:lint_python # test_cli:lint_typescript # HACK: typescript lint disabled because prettier fails",
"test_cli:lint": "run-p test_cli:lint_solidity # test_cli:lint_python test_cli:lint_typescript # HACK: typescript and python lint disabled because formatterr fails",
"test_cli:lint_solidity": "solhint -c ../../contracts/.solhint.json test-cli/fixtures/contracts/*.sol",
"test_cli:lint_typescript": "prettier --check ./test-cli/output/typescript/* --config ../../.prettierrc",
"test_cli:lint_python": "pip install -r test-cli/fixtures/python-requirements.txt && run-p test_cli:lint_python:black test_cli:lint_python:mypy test_cli:lint_python:pylint",

View File

@@ -65,6 +65,22 @@
{
"note": "Stop requiring takerAddress for RFQ-T indicative quotes",
"pr": 2684
},
{
"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
},
{
"note": "Re-worked `Kyber` quotes supporting multiple reserves",
"pr": 2683
}
]
},

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 experimental ABIEncoderV2;
import "./ICurve.sol";
import "./interfaces/ICurve.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "./IKyberStorage.sol";
import "./IKyberHintHandler.sol";
interface IKyberNetwork {
function getContracts()
external
view
returns (
address kyberFeeHandlerAddress,
address kyberDaoAddress,
IKyberHintHandler kyberMatchingEngineAddress,
IKyberStorage kyberStorageAddress,
address gasHelperAddress,
address[] memory kyberProxyAddresses);
}

View File

@@ -1,34 +0,0 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IKyberNetworkProxy {
function kyberNetwork() external view returns (address);
function kyberHintHandler() external view returns (address);
function getExpectedRateAfterFee(
address src,
address dest,
uint256 srcQty,
uint256 platformFeeBps,
bytes calldata hint
) external view returns (uint256 expectedRate);
}

View File

@@ -1,37 +0,0 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IKyberStorage {
function getReserveId(
address reserve
)
external
view
returns (bytes32 reserveId);
function getReserveIdsPerTokenSrc(
address token
)
external
view
returns (bytes32[] memory reserveIds);
}

View File

@@ -1,6 +1,6 @@
/*
Copyright 2019 ZeroEx Intl.
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,10 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IKyberNetwork.sol";
import "./IKyberNetworkProxy.sol";
import "./IKyberStorage.sol";
import "./IKyberHintHandler.sol";
import "./interfaces/IKyberNetwork.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";
@@ -34,73 +31,130 @@ contract KyberSampler is
ApproximateBuys
{
/// @dev Gas limit for Kyber calls.
uint256 constant private KYBER_CALL_GAS = 1500e3; // 1.5m
/// @dev The Kyber Uniswap Reserve address
address constant private KYBER_UNISWAP_RESERVE = 0x31E085Afd48a1d6e51Cc193153d625e8f0514C7F;
/// @dev The Kyber Uniswap V2 Reserve address
address constant private KYBER_UNISWAPV2_RESERVE = 0x10908C875D865C66f271F5d3949848971c9595C9;
/// @dev The Kyber Eth2Dai Reserve address
address constant private KYBER_ETH2DAI_RESERVE = 0x1E158c0e93c30d24e918Ef83d1e0bE23595C3c0f;
uint256 constant private KYBER_CALL_GAS = 500e3; // 500k
/// @dev Sample sell quotes from Kyber.
/// @param reserveId The selected kyber reserve
/// @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.
/// @return hint The hint for the selected reserve
/// @return makerTokenAmounts Maker amounts bought at each taker token amount.
function sampleSellsFromKyberNetwork(
bytes32 reserveId,
address takerToken,
address makerToken,
uint256[] memory takerTokenAmounts
)
public
view
returns (uint256[] memory makerTokenAmounts)
returns (bytes memory hint, uint256[] memory makerTokenAmounts)
{
_assertValidPair(makerToken, takerToken);
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
address wethAddress = _getWethAddress();
uint256 value;
hint = this.encodeKyberHint(reserveId, takerToken, makerToken);
for (uint256 i = 0; i < numSamples; i++) {
if (takerToken == wethAddress || makerToken == wethAddress) {
// Direct ETH based trade
value = _sampleSellFromKyberNetwork(takerToken, makerToken, takerTokenAmounts[i]);
} else {
// Hop to ETH
value = _sampleSellFromKyberNetwork(takerToken, wethAddress, takerTokenAmounts[i]);
if (value != 0) {
value = _sampleSellFromKyberNetwork(wethAddress, makerToken, value);
}
uint256 value = this.sampleSellFromKyberNetwork(hint, takerToken, makerToken, takerTokenAmounts[i]);
// Return early if the source has no liquidity
if (value == 0) {
return (hint, makerTokenAmounts);
}
makerTokenAmounts[i] = value;
}
}
/// @dev Sample buy quotes from Kyber.
/// @param reserveId The selected kyber reserve
/// @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.
/// @return hint The hint for the selected reserve
/// @return takerTokenAmounts Taker amounts sold at each maker token amount.
function sampleBuysFromKyberNetwork(
bytes32 reserveId,
address takerToken,
address makerToken,
uint256[] memory makerTokenAmounts
)
public
view
returns (uint256[] memory takerTokenAmounts)
returns (bytes memory hint, uint256[] memory takerTokenAmounts)
{
_assertValidPair(makerToken, takerToken);
return _sampleApproximateBuys(
hint = this.encodeKyberHint(reserveId, takerToken, makerToken);
takerTokenAmounts = _sampleApproximateBuys(
ApproximateBuyQuoteOpts({
makerTokenData: abi.encode(makerToken),
takerTokenData: abi.encode(takerToken),
makerTokenData: abi.encode(makerToken, hint),
takerTokenData: abi.encode(takerToken, hint),
getSellQuoteCallback: _sampleSellForApproximateBuyFromKyber
}),
makerTokenAmounts
);
return (hint, takerTokenAmounts);
}
function encodeKyberHint(
bytes32 reserveId,
address takerToken,
address makerToken
)
public
view
returns (bytes memory hint)
{
// Build a hint selecting the single reserve
IKyberHintHandler kyberHint = IKyberHintHandler(_getKyberHintHandlerAddress());
// All other reserves should be ignored with this hint
bytes32[] memory selectedReserves = new bytes32[](1);
selectedReserves[0] = reserveId;
bool didSucceed;
bytes memory resultData;
if (takerToken == _getWethAddress()) {
// ETH to Token
(didSucceed, resultData) =
address(kyberHint).staticcall.gas(KYBER_CALL_GAS)(
abi.encodeWithSelector(
IKyberHintHandler(0).buildEthToTokenHint.selector,
makerToken,
IKyberHintHandler.TradeType.MaskIn,
selectedReserves,
new uint256[](0)));
} else if (makerToken == _getWethAddress()) {
// Token to ETH
(didSucceed, resultData) =
address(kyberHint).staticcall.gas(KYBER_CALL_GAS)(
abi.encodeWithSelector(
IKyberHintHandler(0).buildTokenToEthHint.selector,
takerToken,
IKyberHintHandler.TradeType.MaskIn,
selectedReserves,
new uint256[](0)));
} else {
// Token to Token
// We use the same reserve both ways
(didSucceed, resultData) =
address(kyberHint).staticcall.gas(KYBER_CALL_GAS)(
abi.encodeWithSelector(
IKyberHintHandler(0).buildTokenToTokenHint.selector,
takerToken,
IKyberHintHandler.TradeType.MaskIn,
selectedReserves,
new uint256[](0),
makerToken,
IKyberHintHandler.TradeType.MaskIn,
selectedReserves,
new uint256[](0)
)
);
}
// If successful decode the hint
if (didSucceed) {
hint = abi.decode(resultData, (bytes));
}
return hint;
}
function _sampleSellForApproximateBuyFromKyber(
@@ -112,82 +166,40 @@ contract KyberSampler is
view
returns (uint256 buyAmount)
{
(address makerToken, bytes memory hint) =
abi.decode(makerTokenData, (address, bytes));
(address takerToken, ) =
abi.decode(takerTokenData, (address, bytes));
(bool success, bytes memory resultData) =
address(this).staticcall(abi.encodeWithSelector(
this.sampleSellsFromKyberNetwork.selector,
abi.decode(takerTokenData, (address)),
abi.decode(makerTokenData, (address)),
_toSingleValueArray(sellAmount)
this.sampleSellFromKyberNetwork.selector,
hint,
takerToken,
makerToken,
sellAmount
));
if (!success) {
return 0;
}
// solhint-disable-next-line indent
return abi.decode(resultData, (uint256[]))[0];
return abi.decode(resultData, (uint256));
}
function _appendToList(bytes32[] memory list, bytes32 item) private view returns (bytes32[] memory appendedList)
{
appendedList = new bytes32[](list.length + 1);
for (uint256 i = 0; i < list.length; i++) {
appendedList[i] = list[i];
}
appendedList[appendedList.length - 1] = item;
}
function _getKyberAddresses()
private
view
returns (IKyberHintHandler kyberHint, IKyberStorage kyberStorage)
{
(, , kyberHint, kyberStorage, ,) = IKyberNetwork(
IKyberNetworkProxy(_getKyberNetworkProxyAddress()).kyberNetwork()).getContracts();
return (IKyberHintHandler(kyberHint), IKyberStorage(kyberStorage));
}
function _sampleSellFromKyberNetwork(
function sampleSellFromKyberNetwork(
bytes memory hint,
address takerToken,
address makerToken,
uint256 takerTokenAmount
)
private
public
view
returns (uint256 makerTokenAmount)
{
(IKyberHintHandler kyberHint, IKyberStorage kyberStorage) = _getKyberAddresses();
// Ban reserves which can clash with our internal aggregation
bytes32[] memory reserveIds = kyberStorage.getReserveIdsPerTokenSrc(
takerToken == _getWethAddress() ? makerToken : takerToken
);
bytes32[] memory bannedReserveIds = new bytes32[](0);
// Poor mans resize and append
for (uint256 i = 0; i < reserveIds.length; i++) {
if (
reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAP_RESERVE) ||
reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAPV2_RESERVE) ||
reserveIds[i] == kyberStorage.getReserveId(KYBER_ETH2DAI_RESERVE)
) {
bannedReserveIds = _appendToList(bannedReserveIds, reserveIds[i]);
}
}
// Sampler either detects X->ETH/ETH->X
// or subsamples as X->ETH-Y. So token->token here is not possible
bytes memory hint;
if (takerToken == _getWethAddress()) {
// ETH -> X
hint = kyberHint.buildEthToTokenHint(
makerToken,
IKyberHintHandler.TradeType.MaskOut,
bannedReserveIds,
new uint256[](0));
} else {
// X->ETH
hint = kyberHint.buildEthToTokenHint(
takerToken,
IKyberHintHandler.TradeType.MaskOut,
bannedReserveIds,
new uint256[](0));
// If there is no hint do not continue
if (hint.length == 0) {
return 0;
}
(bool didSucceed, bytes memory resultData) =
_getKyberNetworkProxyAddress().staticcall.gas(KYBER_CALL_GAS)(
abi.encodeWithSelector(

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./IMultiBridge.sol";
import "./interfaces/IMultiBridge.sol";
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-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./IUniswapExchangeQuotes.sol";
import "./interfaces/IUniswapExchangeQuotes.sol";
import "./SamplerUtils.sol";

View File

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

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

@@ -18,11 +18,29 @@
pragma solidity ^0.5.9;
// Keepin everything together
interface IKyberNetwork {
}
interface IKyberNetworkProxy {
function getExpectedRateAfterFee(
address src,
address dest,
uint256 srcQty,
uint256 platformFeeBps,
bytes calldata hint
)
external
view
returns (uint256 expectedRate);
}
interface IKyberHintHandler {
function kyberStorage() external returns (address);
enum TradeType {BestOfAll, MaskIn, MaskOut, Split}
function buildTokenToEthHint(
@@ -30,14 +48,20 @@ interface IKyberHintHandler {
TradeType tokenToEthType,
bytes32[] calldata tokenToEthReserveIds,
uint256[] calldata tokenToEthSplits
) external view returns (bytes memory hint);
)
external
view
returns (bytes memory hint);
function buildEthToTokenHint(
address tokenDest,
TradeType ethToTokenType,
bytes32[] calldata ethToTokenReserveIds,
uint256[] calldata ethToTokenSplits
) external view returns (bytes memory hint);
)
external
view
returns (bytes memory hint);
function buildTokenToTokenHint(
address tokenSrc,
@@ -48,5 +72,8 @@ interface IKyberHintHandler {
TradeType ethToTokenType,
bytes32[] calldata ethToTokenReserveIds,
uint256[] calldata ethToTokenSplits
) external view returns (bytes memory hint);
)
external
view
returns (bytes memory hint);
}

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-libs/contracts/src/LibOrder.sol";
import "../src/ERC20BridgeSampler.sol";
import "../src/IEth2Dai.sol";
import "../src/IKyberNetworkProxy.sol";
import "../src/IUniswapV2Router01.sol";
import "../src/interfaces/IEth2Dai.sol";
import "../src/interfaces/IKyberNetwork.sol";
import "../src/interfaces/IUniswapV2Router01.sol";
library LibDeterministicQuotes {
@@ -253,76 +253,40 @@ contract TestERC20BridgeSamplerKyberNetwork is
enum TradeType {BestOfAll, MaskIn, MaskOut, Split}
function kyberNetwork()
external
view
returns (address)
{
return address(this);
}
// IKyberNetwork
function getContracts()
external
view
returns (
address kyberFeeHandlerAddress,
address kyberDaoAddress,
address kyberMatchingEngineAddress,
address kyberStorageAddress,
address gasHelperAddress,
address[] memory kyberProxyAddresses
)
{
return (kyberFeeHandlerAddress,
kyberDaoAddress,
address(this),
address(this),
gasHelperAddress,
kyberProxyAddresses
);
}
// IKyberStorage
function getReserveIdsPerTokenSrc(
address /* token */
)
external
view
returns (bytes32[] memory reserveIds)
{
return reserveIds;
}
function getReserveId(
address /* reserve */
)
external
view
returns (bytes32 reserveId)
{
return reserveId;
}
// IKyberHintHandler
function buildTokenToEthHint(
address /* tokenSrc */,
address tokenSrc,
TradeType /* tokenToEthType */,
bytes32[] calldata /* tokenToEthReserveIds */,
uint256[] calldata /* tokenToEthSplits */
) external view returns (bytes memory hint)
{
return hint;
return abi.encode(tokenSrc);
}
function buildEthToTokenHint(
address /* tokenDest */,
address tokenDest,
TradeType /* ethToTokenType */,
bytes32[] calldata /* ethToTokenReserveIds */,
uint256[] calldata /* ethToTokenSplits */
) external view returns (bytes memory hint)
{
return hint;
return abi.encode(tokenDest);
}
// IKyberHintHandler
function buildTokenToTokenHint(
address tokenSrc,
TradeType /* tokenToEthType */,
bytes32[] calldata /* tokenToEthReserveIds */,
uint256[] calldata /* tokenToEthSplits */,
address /* tokenDest */,
TradeType /* EthToTokenType */,
bytes32[] calldata /* EthToTokenReserveIds */,
uint256[] calldata /* EthToTokenSplits */
) external view returns (bytes memory hint)
{
return abi.encode(tokenSrc);
}
// Deterministic `IKyberNetworkProxy.getExpectedRateAfterFee()`.
@@ -375,6 +339,14 @@ contract TestERC20BridgeSamplerKyberNetwork is
{
return address(this);
}
function _getKyberHintHandlerAddress()
internal
view
returns (address)
{
return address(this);
}
}
@@ -533,4 +505,13 @@ contract TestERC20BridgeSampler is
{
return address(kyber);
}
// Overriden to point to a custom contract.
function _getKyberHintHandlerAddress()
internal
view
returns (address kyberAddress)
{
return address(kyber);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -132,12 +132,49 @@ export const MAINNET_CURVE_INFOS: { [name: string]: CurveInfo } = {
},
};
export const MAINNET_KYBER_RESERVE_IDS: { [name: string]: string } = {
Reserve1: '0xff4b796265722046707200000000000000000000000000000000000000000000',
Reserve2: '0xffabcd0000000000000000000000000000000000000000000000000000000000',
Reserve3: '0xff4f6e65426974205175616e7400000000000000000000000000000000000000',
};
export const MAINNET_KYBER_TOKEN_RESERVE_IDS: { [token: string]: string } = {
// USDC
['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']:
'0xaa55534443303041505200000000000000000000000000000000000000000000',
// AMPL
['0xd46ba6d942050d489dbd938a2c909a5d5039a161']:
'0xaad46ba6d942050d489dbd938a2c909a5d5039a1610000000000000000000000',
// UBT
['0x8400d94a5cb0fa0d041a3788e395285d61c9ee5e']:
'0xaa55425400000000000000000000000000000000000000000000000000000000',
// ANT
['0x960b236a07cf122663c4303350609a66a7b288c0']:
'0xaa414e5400000000000000000000000000000000000000000000000000000000',
// KNC
['0xdd974d5c2e2928dea5f71b9825b8b646686bd200']:
'0xaa4b4e435f4d4547414c41444f4e000000000000000000000000000000000000',
// sUSD
['0x57ab1ec28d129707052df4df418d58a2d46d5f51']:
'0xaa73555344000000000000000000000000000000000000000000000000000000',
// SNX
['0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f']:
'0xaa534e5800000000000000000000000000000000000000000000000000000000',
// REN
['0x408e41876cccdc0f92210600ef50372656052a38']:
'0xaa72656e00000000000000000000000000000000000000000000000000000000',
// BAND
['0xba11d00c5f74255f56a5e366f4f77f5a186d7f55']:
'0xaa42414e44000000000000000000000000000000000000000000000000000000',
};
export const ERC20_PROXY_ID = '0xf47261b0';
export const WALLET_SIGNATURE = '0x04';
export const ONE_ETHER = new BigNumber(1e18);
export const NEGATIVE_INF = new BigNumber('-Infinity');
export const POSITIVE_INF = new BigNumber('Infinity');
export const ZERO_AMOUNT = new BigNumber(0);
export const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
export const ONE_HOUR_IN_SECONDS = 60 * 60;
export const ONE_SECOND_MS = 1000;
export const NULL_BYTES = '0x';

View File

@@ -4,7 +4,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
@@ -155,6 +155,22 @@ function dexQuotesToPaths(
return paths;
}
export function getTwoHopAdjustedRate(
side: MarketOperation,
twoHopQuote: DexSample<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 {
switch (source) {
case ERC20BridgeSource.Uniswap:

View File

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

View File

@@ -0,0 +1,10 @@
import { MAINNET_KYBER_RESERVE_IDS, MAINNET_KYBER_TOKEN_RESERVE_IDS } from './constants';
// tslint:disable completed-docs
export function getKyberReserveIdsForPair(takerToken: string, makerToken: string): string[] {
return [
...Object.values(MAINNET_KYBER_RESERVE_IDS),
MAINNET_KYBER_TOKEN_RESERVE_IDS[makerToken.toLowerCase()],
MAINNET_KYBER_TOKEN_RESERVE_IDS[takerToken.toLowerCase()],
].filter(t => t);
}

View File

@@ -0,0 +1,55 @@
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;
if (twoHopQuotes.length === 0) {
return { adjustedRate: ZERO_AMOUNT, quote: undefined };
}
const best = twoHopQuotes
.map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule))
.reduce(
(prev, curr, i) =>
curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev,
{
adjustedRate: getTwoHopAdjustedRate(side, twoHopQuotes[0], inputAmount, ethToOutputRate, feeSchedule),
quote: twoHopQuotes[0],
},
);
return best;
}

View File

@@ -8,6 +8,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import {
ERC20_PROXY_ID,
MAX_UINT256,
NULL_ADDRESS,
NULL_BYTES,
ONE_HOUR_IN_SECONDS,
@@ -23,10 +24,13 @@ import {
BancorFillData,
CollapsedFill,
CurveFillData,
DexSample,
ERC20BridgeSource,
Fill,
KyberFillData,
LiquidityProviderFillData,
MultiBridgeFillData,
MultiHopFillData,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
@@ -150,6 +154,7 @@ export interface CreateOrderFromPathOpts {
// Convert sell fills into orders.
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const collapsedPath = collapsePath(path);
const orders: OptimizedMarketOrder[] = [];
for (let i = 0; i < collapsedPath.length; ) {
@@ -168,7 +173,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
}
// Always use DexForwarderBridge unless configured not to
if (!opts.shouldBatchBridgeOrders) {
orders.push(createBridgeOrder(contiguousBridgeFills[0], opts));
orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
i += 1;
} else {
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
@@ -178,9 +183,36 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
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 {
const source = fill.source;
switch (source) {
switch (fill.source) {
case ERC20BridgeSource.Eth2Dai:
return opts.contractAddresses.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
@@ -209,8 +241,12 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath
throw new Error(AggregationError.NoBridgeForSource);
}
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
function createBridgeOrder(
fill: CollapsedFill,
makerToken: string,
takerToken: string,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder {
const bridgeAddress = getBridgeAddressFromFill(fill, opts);
let makerAssetData;
@@ -260,6 +296,14 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
createMultiBridgeData(takerToken, makerToken),
);
break;
case ERC20BridgeSource.Kyber:
const kyberFillData = (fill as CollapsedFill<KyberFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createKyberBridgeData(takerToken, kyberFillData.hint),
);
break;
default:
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
@@ -277,7 +321,7 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
takerAssetAmount: slippedTakerAssetAmount,
fillableMakerAssetAmount: slippedMakerAssetAmount,
fillableTakerAssetAmount: slippedTakerAssetAmount,
...createCommonBridgeOrderFields(opts),
...createCommonBridgeOrderFields(opts.orderDomain),
};
}
@@ -290,7 +334,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
calls: [],
};
for (const fill of fills) {
const bridgeOrder = createBridgeOrder(fill, opts);
const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts);
totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount);
totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount);
const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow(
@@ -318,7 +362,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
takerAssetAmount: totalTakerAssetAmount,
fillableMakerAssetAmount: totalMakerAssetAmount,
fillableTakerAssetAmount: totalTakerAssetAmount,
...createCommonBridgeOrderFields(opts),
...createCommonBridgeOrderFields(opts.orderDomain),
};
}
@@ -358,6 +402,11 @@ function createBancorBridgeData(path: string[], networkAddress: string): string
return encoder.encode({ path, networkAddress });
}
function createKyberBridgeData(fromTokenAddress: string, hint: string): string {
const encoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }, { name: 'hint', type: 'bytes' }]);
return encoder.encode({ fromTokenAddress, hint });
}
function createCurveBridgeData(
curveAddress: string,
exchangeFunctionSelector: string,
@@ -395,7 +444,7 @@ function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFrom
// Taker asset amount.
opts.side === MarketOperation.Sell
? fill.input
: fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP),
: BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256),
];
}
@@ -414,7 +463,7 @@ type CommonBridgeOrderFields = Pick<
>
>;
function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields {
return {
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
@@ -428,7 +477,7 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...opts.orderDomain,
...orderDomain,
};
}

View File

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

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 { QuoteRequestor } from '../../utils/quote_requestor';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { QuoteReport } from '../quote_report_generator';
/**
@@ -41,6 +40,7 @@ export enum ERC20BridgeSource {
Bancor = 'Bancor',
MStable = 'mStable',
Mooniswap = 'Mooniswap',
MultiHop = 'MultiHop',
}
// tslint:disable: enum-naming
@@ -72,6 +72,11 @@ export interface CurveInfo {
// Internal `fillData` field for `Fill` objects.
export interface FillData {}
export interface SourceInfo<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData?: TFillData;
}
// `FillData` for native fills.
export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts;
@@ -103,19 +108,33 @@ export interface BancorFillData extends FillData {
networkAddress: string;
}
export interface KyberFillData extends FillData {
hint: string;
reserveId: string;
}
export interface Quote<TFillData = FillData> {
amount: BigNumber;
fillData?: TFillData;
}
export interface HopInfo {
sourceIndex: BigNumber;
returnData: string;
}
export interface MultiHopFillData extends FillData {
firstHopSource: SourceQuoteOperation;
secondHopSource: SourceQuoteOperation;
intermediateToken: string;
}
/**
* Represents an individual DEX sample from the sampler contract.
*/
export interface DexSample<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
export interface DexSample<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
input: BigNumber;
output: BigNumber;
fillData?: TFillData;
}
/**
@@ -131,7 +150,7 @@ export enum FillFlags {
/**
* 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.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@@ -148,25 +167,16 @@ export interface Fill<TFillData extends FillData = FillData> {
parent?: Fill;
// The index of the fill in the original path.
index: number;
// The source of the fill. See `ERC20BridgeSource`.
source: ERC20BridgeSource;
// Data associated with this this Fill object. Used to reconstruct orders
// from paths.
fillData?: TFillData;
}
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill<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.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string;
/**
* The source DEX.
*/
source: ERC20BridgeSource;
/**
* Total input amount (sum of `subFill`s)
*/
@@ -182,8 +192,6 @@ export interface CollapsedFill<TFillData extends FillData = FillData> {
input: BigNumber;
output: BigNumber;
}>;
fillData?: TFillData;
}
/**
@@ -273,17 +281,19 @@ export interface GetMarketOrdersOpts {
* A composable operation the be run in `DexOrderSampler.executeAsync()`.
*/
export interface BatchedOperation<TResult> {
encodeCall(contract: ERC20BridgeSamplerContract): string;
handleCallResultsAsync(contract: ERC20BridgeSamplerContract, callResults: string): Promise<TResult>;
encodeCall(): string;
handleCallResults(callResults: string): TResult;
}
export interface SourceQuoteOperation<TFillData extends FillData = FillData>
extends BatchedOperation<Array<Quote<TFillData>>> {
source: ERC20BridgeSource;
extends BatchedOperation<BigNumber[]>,
SourceInfo<TFillData> {
readonly source: ERC20BridgeSource;
}
export interface OptimizedOrdersAndQuoteReport {
export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean;
quoteReport: QuoteReport;
}
@@ -305,4 +315,9 @@ export interface MarketSideLiquidity {
ethToOutputRate: BigNumber;
ethToInputRate: BigNumber;
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 { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { ERC20BridgeSource, SignedOrder } from '..';
import { MarketOperation } from '../types';
import { CollapsedFill, DexSample, NativeCollapsedFill } from './market_operation_utils/types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
} from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource {
@@ -14,6 +20,13 @@ export interface BridgeReportSource {
takerAmount: BigNumber;
}
export interface MultiHopReportSource {
liquiditySource: ERC20BridgeSource.MultiHop;
makerAmount: BigNumber;
takerAmount: BigNumber;
hopSources: ERC20BridgeSource[];
}
interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber;
@@ -29,7 +42,11 @@ export interface NativeRFQTReportSource extends NativeReportSourceBase {
isRfqt: true;
makerUri: string;
}
export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource;
export type QuoteReportSource =
| BridgeReportSource
| NativeOrderbookReportSource
| NativeRFQTReportSource
| MultiHopReportSource;
export interface QuoteReport {
sourcesConsidered: QuoteReportSource[];
@@ -47,115 +64,152 @@ const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefine
}
};
export class QuoteReportGenerator {
private readonly _dexQuotes: DexSample[];
private readonly _nativeOrders: SignedOrder[];
private readonly _orderHashesToFillableAmounts: { [orderHash: string]: BigNumber };
private readonly _marketOperation: MarketOperation;
private readonly _collapsedFills: CollapsedFill[];
private readonly _quoteRequestor?: QuoteRequestor;
constructor(
marketOperation: MarketOperation,
dexQuotes: DexSample[],
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
collapsedFills: CollapsedFill[],
quoteRequestor?: QuoteRequestor,
) {
this._dexQuotes = dexQuotes;
this._nativeOrders = nativeOrders;
this._marketOperation = marketOperation;
this._quoteRequestor = quoteRequestor;
this._collapsedFills = collapsedFills;
// convert order fillable amount array to easy to look up hash
if (orderFillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
this._orderHashesToFillableAmounts = {};
return;
}
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
this._orderHashesToFillableAmounts = orderHashesToFillableAmounts;
/**
* Generates a report of sources considered while computing the optimized
* swap quote, and the sources ultimately included in the computed quote.
*/
export function generateQuoteReport(
marketOperation: MarketOperation,
dexQuotes: DexSample[],
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>,
quoteRequestor?: QuoteRequestor,
): QuoteReport {
// convert order fillable amount array to easy to look up hash
if (orderFillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
throw new Error('orderFillableAmounts must be the same length as nativeOrders');
}
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
public generateReport(): QuoteReport {
const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq));
const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no));
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
const nativeOrderSourcesConsidered = nativeOrders.map(order =>
_nativeOrderToReportSource(
order,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(order)],
quoteRequestor,
),
);
const multiHopSourcesConsidered = multiHopQuotes.map(quote =>
_multiHopSampleToReportSource(quote, marketOperation),
);
const sourcesConsidered = [
...dexReportSourcesConsidered,
...nativeOrderSourcesConsidered,
...multiHopSourcesConsidered,
];
const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered];
const sourcesDelivered = this._collapsedFills.map(collapsedFill => {
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) {
return this._nativeOrderToReportSource(foundNativeOrder);
return _nativeOrderToReportSource(
foundNativeOrder,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(foundNativeOrder)],
quoteRequestor,
);
} else {
return this._dexSampleToReportSource(collapsedFill);
return _dexSampleToReportSource(collapsedFill, marketOperation);
}
});
} else {
sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)];
}
return {
sourcesConsidered,
sourcesDelivered,
};
}
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeReportSource {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return {
sourcesConsidered,
sourcesDelivered,
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
};
}
private _dexSampleToReportSource(ds: DexSample): BridgeReportSource {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
throw new Error(`Unexpected liquidity source Native`);
}
// input and output map to different values
// based on the market operation
if (this._marketOperation === MarketOperation.Buy) {
return {
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
};
} else if (this._marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
};
} else {
throw new Error(`Unexpected marketOperation ${this._marketOperation}`);
}
}
private _nativeOrderToReportSource(nativeOrder: SignedOrder): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: this._orderHashesToFillableAmounts[orderHash],
nativeOrder,
orderHash,
} else if (marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = this._quoteRequestor && this._quoteRequestor.getMakerUriForOrderHash(orderHash);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
return rfqtSource;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
}
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
function _multiHopSampleToReportSource(
ds: DexSample<MultiHopFillData>,
marketOperation: MarketOperation,
): MultiHopReportSource {
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!;
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.input,
takerAmount: ds.output,
hopSources: [firstHop.source, secondHop.source],
};
} else if (marketOperation === MarketOperation.Sell) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.output,
takerAmount: ds.input,
hopSources: [firstHop.source, secondHop.source],
};
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
function _nativeOrderToReportSource(
nativeOrder: SignedOrder,
fillableAmount: BigNumber,
quoteRequestor?: QuoteRequestor,
): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: fillableAmount,
nativeOrder,
orderHash,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderHash(orderHash);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
return rfqtSource;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
}
}

View File

@@ -350,10 +350,8 @@ export class QuoteRequestor {
switch (quoteType) {
case 'firm':
return 'quote';
break;
case 'indicative':
return 'price';
break;
default:
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,
...quoteInfo.opts,
};
const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier);
const result = fillQuoteOrders(
createBestCaseFillOrderCalls(quoteInfo),
quoteInfo.fillAmount,
quoteInfo.gasPrice.times(opts.protocolFeeMultiplier),
protocolFeePerFillOrder,
opts.gasSchedule,
);
return fromIntermediateQuoteFillResult(result, quoteInfo);

View File

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

View File

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

View File

@@ -6,17 +6,16 @@
import { ContractArtifact } from 'ethereum-types';
import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json';
import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json';
import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json';
import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json';
import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json';
import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json';
import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json';
import * as IBalancer from '../test/generated-artifacts/IBalancer.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json';
import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json';
import * as IKyberNetwork from '../test/generated-artifacts/IKyberNetwork.json';
import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json';
import * as IKyberStorage from '../test/generated-artifacts/IKyberStorage.json';
import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json';
import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json';
import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json';
@@ -33,28 +32,16 @@ import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSamp
import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json';
import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json';
import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json';
import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json';
import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json';
export const artifacts = {
ApproximateBuys: ApproximateBuys as ContractArtifact,
BalancerSampler: BalancerSampler as ContractArtifact,
CurveSampler: CurveSampler as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact,
Eth2DaiSampler: Eth2DaiSampler as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact,
IKyberHintHandler: IKyberHintHandler as ContractArtifact,
IKyberNetwork: IKyberNetwork as ContractArtifact,
IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact,
IKyberStorage: IKyberStorage as ContractArtifact,
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IMStable: IMStable as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact,
IMultiBridge: IMultiBridge as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
KyberSampler: KyberSampler as ContractArtifact,
LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact,
MStableSampler: MStableSampler as ContractArtifact,
@@ -62,8 +49,21 @@ export const artifacts = {
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact,
TwoHopSampler: TwoHopSampler as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
IBalancer: IBalancer as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact,
IKyberNetwork: IKyberNetwork as ContractArtifact,
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IMStable: IMStable as ContractArtifact,
IMultiBridge: IMultiBridge as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact,
TestNativeOrderSampler: TestNativeOrderSampler as ContractArtifact,
};

View File

@@ -81,24 +81,25 @@ blockchainTests.skip('Mainnet Sampler Tests', env => {
const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f';
const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const RESERVE_ID = '0xff4b796265722046707200000000000000000000000000000000000000000000';
describe('sampleSellsFromKyberNetwork()', () => {
it('samples sells from Kyber DAI->WETH', async () => {
const samples = await testContract
.sampleSellsFromKyberNetwork(DAI, WETH, [toBaseUnitAmount(1)])
const [, samples] = await testContract
.sampleSellsFromKyberNetwork(RESERVE_ID, DAI, WETH, [toBaseUnitAmount(1)])
.callAsync({ overrides });
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
});
it('samples sells from Kyber WETH->DAI', async () => {
const samples = await testContract
.sampleSellsFromKyberNetwork(WETH, DAI, [toBaseUnitAmount(1)])
const [, samples] = await testContract
.sampleSellsFromKyberNetwork(RESERVE_ID, WETH, DAI, [toBaseUnitAmount(1)])
.callAsync({ overrides });
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
});
it('samples sells from Kyber DAI->USDC', async () => {
const samples = await testContract
.sampleSellsFromKyberNetwork(DAI, USDC, [toBaseUnitAmount(1)])
const [, samples] = await testContract
.sampleSellsFromKyberNetwork(RESERVE_ID, DAI, USDC, [toBaseUnitAmount(1)])
.callAsync({ overrides });
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
@@ -109,8 +110,8 @@ blockchainTests.skip('Mainnet Sampler Tests', env => {
it('samples buys from Kyber WETH->DAI', async () => {
// From ETH to DAI
// I want to buy 1 DAI
const samples = await testContract
.sampleBuysFromKyberNetwork(WETH, DAI, [toBaseUnitAmount(1)])
const [, samples] = await testContract
.sampleBuysFromKyberNetwork(RESERVE_ID, WETH, DAI, [toBaseUnitAmount(1)])
.callAsync({ overrides });
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
@@ -119,8 +120,8 @@ blockchainTests.skip('Mainnet Sampler Tests', env => {
it('samples buys from Kyber DAI->WETH', async () => {
// From USDC to DAI
// I want to buy 1 WETH
const samples = await testContract
.sampleBuysFromKyberNetwork(DAI, WETH, [toBaseUnitAmount(1)])
const [, samples] = await testContract
.sampleBuysFromKyberNetwork(RESERVE_ID, DAI, WETH, [toBaseUnitAmount(1)])
.callAsync({ overrides });
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);

View File

@@ -37,6 +37,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR';
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const INTERMEDIATE_TOKEN = randomAddress();
const KYBER_RESERVE_ID = '0x';
before(async () => {
testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync(
@@ -262,6 +264,33 @@ blockchainTests('erc20-bridge-sampler', env => {
await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 });
}
function expectQuotesWithinRange(
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
): void {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
}
describe('getOrderFillableTakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
@@ -302,31 +331,25 @@ blockchainTests('erc20-bridge-sampler', env => {
});
it('throws if tokens are the same', async () => {
const tx = testContract.sampleSellsFromKyberNetwork(MAKER_TOKEN, MAKER_TOKEN, []).callAsync();
const tx = testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, MAKER_TOKEN, MAKER_TOKEN, [])
.callAsync();
return expect(tx).to.revertWith(INVALID_TOKEN_PAIR_ERROR);
});
it('can return no quotes', async () => {
const quotes = await testContract.sampleSellsFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync();
expect(quotes).to.deep.eq([]);
});
it('can quote token -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [takerToEthQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts);
const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, MAKER_TOKEN, ['Kyber'], takerToEthQuotes);
const quotes = await testContract
.sampleSellsFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, [])
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
expect(quotes).to.deep.eq([]);
});
it('returns zero if token -> token fails', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleSellsFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -334,8 +357,17 @@ blockchainTests('erc20-bridge-sampler', env => {
it('can quote token -> ETH', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts);
const quotes = await testContract
.sampleSellsFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
it('can quote token -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Kyber'], sampleAmounts);
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -344,8 +376,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleSellsFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -353,8 +385,8 @@ blockchainTests('erc20-bridge-sampler', env => {
it('can quote ETH -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Kyber'], sampleAmounts);
const quotes = await testContract
.sampleSellsFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -363,8 +395,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleSellsFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleSellsFromKyberNetwork(KYBER_RESERVE_ID, WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -377,47 +409,24 @@ blockchainTests('erc20-bridge-sampler', env => {
});
it('throws if tokens are the same', async () => {
const tx = testContract.sampleBuysFromKyberNetwork(MAKER_TOKEN, MAKER_TOKEN, []).callAsync();
const tx = testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, MAKER_TOKEN, MAKER_TOKEN, [])
.callAsync();
return expect(tx).to.revertWith(INVALID_TOKEN_PAIR_ERROR);
});
it('can return no quotes', async () => {
const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync();
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, [])
.callAsync();
expect(quotes).to.deep.eq([]);
});
const expectQuotesWithinRange = (
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
) => {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
};
it('can quote token -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [ethToMakerQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, MAKER_TOKEN, ['Kyber'], sampleAmounts);
const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], ethToMakerQuotes);
const quotes = await testContract
.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Kyber'], sampleAmounts);
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
.callAsync();
expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE);
});
@@ -426,8 +435,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -435,8 +444,8 @@ blockchainTests('erc20-bridge-sampler', env => {
it('can quote token -> ETH', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts);
const quotes = await testContract
.sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
.callAsync();
expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE);
});
@@ -445,8 +454,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, TAKER_TOKEN, WETH_ADDRESS, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -454,8 +463,8 @@ blockchainTests('erc20-bridge-sampler', env => {
it('can quote ETH -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const [expectedQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Kyber'], sampleAmounts);
const quotes = await testContract
.sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
.callAsync();
expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE);
});
@@ -464,8 +473,8 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
const [, quotes] = await testContract
.sampleBuysFromKyberNetwork(KYBER_RESERVE_ID, WETH_ADDRESS, TAKER_TOKEN, sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@@ -803,7 +812,7 @@ blockchainTests('erc20-bridge-sampler', env => {
});
});
describe('getLiquidityProviderFromRegistry', () => {
describe('liquidity provider', () => {
const xAsset = randomAddress();
const yAsset = randomAddress();
const sampleAmounts = getSampleAmounts(yAsset);
@@ -829,42 +838,28 @@ blockchainTests('erc20-bridge-sampler', env => {
.awaitTransactionSuccessAsync();
});
it('should be able to get the liquidity provider', async () => {
const xyLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, xAsset, yAsset)
.callAsync();
const yxLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, xAsset)
.callAsync();
const unknownLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, randomAddress())
.callAsync();
expect(xyLiquidityProvider).to.eq(liquidityProvider.address);
expect(yxLiquidityProvider).to.eq(liquidityProvider.address);
expect(unknownLiquidityProvider).to.eq(constants.NULL_ADDRESS);
});
it('should be able to query sells from the liquidity provider', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleSellsFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach((value, idx) => {
quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].minus(1));
});
expect(providerAddress).to.equal(liquidityProvider.address);
});
it('should be able to query buys from the liquidity provider', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach((value, idx) => {
quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1));
});
expect(providerAddress).to.equal(liquidityProvider.address);
});
it('should just return zeros if the liquidity provider cannot be found', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(
registryContract.address,
yAsset,
@@ -872,18 +867,20 @@ blockchainTests('erc20-bridge-sampler', env => {
sampleAmounts,
)
.callAsync();
result.forEach(value => {
quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
});
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
});
it('should just return zeros if the registry does not exist', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach(value => {
quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
});
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
});
});
@@ -1033,4 +1030,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,
} from '../src/utils/market_operation_utils/balancer_utils';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types';
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockBancorService } from './utils/mock_bancor_service';
@@ -108,7 +108,7 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
@@ -124,7 +124,7 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
@@ -135,45 +135,46 @@ describe('DexSampler tests', () => {
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleSellsFromKyberNetwork: (takerToken, makerToken, fillAmounts) => {
sampleSellsFromKyberNetwork: (_reserveId, takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return expectedMakerFillAmounts;
return ['0x', expectedMakerFillAmounts];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getKyberSellQuotes(
dexOrderSampler.getKyberSellQuotes(
'0x',
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getLiquidityProviderSellQuotes()', async () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(1001)];
return [[toBaseUnitAmount(1001)], poolAddress];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@@ -183,7 +184,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress },
},
],
]);
@@ -193,23 +194,23 @@ describe('DexSampler tests', () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(999)];
return [[toBaseUnitAmount(999)], poolAddress];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
dexOrderSampler.getBuyQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@@ -219,7 +220,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress },
},
],
]);
@@ -246,13 +247,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.MultiBridge],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
randomAddress(),
dexOrderSampler.balancerPoolsCache,
randomAddress(),
multiBridge,
),
@@ -263,7 +263,7 @@ describe('DexSampler tests', () => {
source: 'MultiBridge',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress: multiBridge },
},
],
]);
@@ -284,13 +284,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiSellQuotes(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
dexOrderSampler.getEth2DaiSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapSellQuotes()', async () => {
@@ -308,13 +304,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapSellQuotes(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
dexOrderSampler.getUniswapSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapV2SellQuotes()', async () => {
@@ -331,12 +323,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapV2SellQuotes(
dexOrderSampler.getUniswapV2SellQuotes(
[expectedMakerToken, expectedTakerToken],
expectedTakerFillAmounts,
),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getEth2DaiBuyQuotes()', async () => {
@@ -354,13 +346,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiBuyQuotes(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
dexOrderSampler.getEth2DaiBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
});
it('getUniswapBuyQuotes()', async () => {
@@ -378,13 +366,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapBuyQuotes(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
dexOrderSampler.getUniswapBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
});
interface RatesBySource {
@@ -394,12 +378,7 @@ describe('DexSampler tests', () => {
it('getSellQuotes()', async () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const sources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.UniswapV2,
];
const sources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2];
const ratesBySource: RatesBySource = {
[ERC20BridgeSource.Kyber]: getRandomFloat(0, 100),
[ERC20BridgeSource.Eth2Dai]: getRandomFloat(0, 100),
@@ -408,12 +387,6 @@ describe('DexSampler tests', () => {
};
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const sampler = new MockSamplerContract({
sampleSellsFromKyberNetwork: (takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Kyber]).integerValue());
},
sampleSellsFromUniswap: (takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
@@ -440,13 +413,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
sources,
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = sources.map(s =>
@@ -457,7 +429,7 @@ describe('DexSampler tests', () => {
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
: {},
})),
);
const uniswapV2ETHQuotes = [
@@ -471,7 +443,8 @@ describe('DexSampler tests', () => {
})),
];
// extra quote for Uniswap V2, which provides a direct quote (tokenA -> tokenB) AND an ETH quote (tokenA -> ETH -> tokenB)
expect(quotes).to.have.lengthOf(sources.length + 1);
const additionalSourceCount = 1;
expect(quotes).to.have.lengthOf(sources.length + additionalSourceCount);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
});
it('getSellQuotes() uses samples from Balancer', async () => {
@@ -488,19 +461,14 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}),
undefined, // sampler overrides
undefined, // bancor service
undefined,
undefined,
balancerPoolsCache,
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.Balancer],
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
const quotes = await dexOrderSampler.getBalancerSellQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
);
const expectedQuotes = pools.map(p =>
expectedTakerFillAmounts.map(a => ({
@@ -535,28 +503,17 @@ describe('DexSampler tests', () => {
bancorService,
undefined, // balancer cache
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.Bancor],
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
undefined, // balancer pools cache
undefined, // liquidity provider registry address
undefined, // multibridge address
bancorService,
),
const quotes = await dexOrderSampler.getBancorSellQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
);
const expectedQuotes = [
expectedTakerFillAmounts.map(a => ({
source: ERC20BridgeSource.Bancor,
input: a,
output: a.multipliedBy(rate),
fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress },
})),
];
expect(quotes).to.have.lengthOf(1); // one set per pool
const expectedQuotes = expectedTakerFillAmounts.map(a => ({
source: ERC20BridgeSource.Bancor,
input: a,
output: a.multipliedBy(rate),
fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress },
}));
expect(quotes).to.deep.eq(expectedQuotes);
});
it('getBuyQuotes()', async () => {
@@ -596,13 +553,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
dexOrderSampler.getBuyQuotes(
sources,
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = sources.map(s =>
@@ -613,7 +569,7 @@ describe('DexSampler tests', () => {
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
: {},
})),
);
const uniswapV2ETHQuotes = [
@@ -644,19 +600,14 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}),
undefined, // sampler overrides
undefined, // bancor service
undefined,
undefined,
balancerPoolsCache,
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
[ERC20BridgeSource.Balancer],
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
const quotes = await dexOrderSampler.getBalancerBuyQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
);
const expectedQuotes = pools.map(p =>
expectedMakerFillAmounts.map(a => ({
@@ -689,8 +640,8 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts);
expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts);

View File

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

View File

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

View File

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

@@ -23,13 +23,25 @@ export type SampleBuysHandler = (
makerToken: string,
makerTokenAmounts: BigNumber[],
) => SampleResults;
export type SampleSellsKyberHandler = (
reserveId: string,
takerToken: string,
makerToken: string,
takerTokenAmounts: BigNumber[],
) => [string, SampleResults];
export type SampleBuysKyberHandler = (
reserveId: string,
takerToken: string,
makerToken: string,
makerTokenAmounts: BigNumber[],
) => [string, SampleResults];
export type SampleBuysMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults;
export type SampleSellsLPHandler = (
registryAddress: string,
takerToken: string,
makerToken: string,
takerTokenAmounts: BigNumber[],
) => SampleResults;
) => [SampleResults, string];
export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults;
export type SampleSellsMBHandler = (
multiBridgeAddress: string,
@@ -40,7 +52,7 @@ export type SampleSellsMBHandler = (
) => SampleResults;
const DUMMY_PROVIDER = {
sendAsync: (...args: any[]): any => {
sendAsync: (..._args: any[]): any => {
/* no-op */
},
};
@@ -48,7 +60,7 @@ const DUMMY_PROVIDER = {
interface Handlers {
getOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler;
getOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler;
sampleSellsFromKyberNetwork: SampleSellsHandler;
sampleSellsFromKyberNetwork: SampleSellsKyberHandler;
sampleSellsFromLiquidityProviderRegistry: SampleSellsLPHandler;
sampleSellsFromMultiBridge: SampleSellsMBHandler;
sampleSellsFromEth2Dai: SampleSellsHandler;
@@ -73,7 +85,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
public batchCall(callDatas: string[]): ContractFunctionObj<string[]> {
return {
...super.batchCall(callDatas),
callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
callAsync: async (..._callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
};
}
@@ -104,13 +116,15 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
}
public sampleSellsFromKyberNetwork(
reserveId: string,
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<[string, BigNumber[]]> {
return this._wrapCall(
super.sampleSellsFromKyberNetwork,
this._handlers.sampleSellsFromKyberNetwork,
reserveId,
takerToken,
makerToken,
takerAssetAmounts,
@@ -121,7 +135,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromEth2Dai,
this._handlers.sampleSellsFromEth2Dai,
@@ -135,7 +149,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromUniswap,
this._handlers.sampleSellsFromUniswap,
@@ -145,10 +159,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
);
}
public sampleSellsFromUniswapV2(
path: string[],
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
public sampleSellsFromUniswapV2(path: string[], takerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromUniswapV2,
this._handlers.sampleSellsFromUniswapV2,
@@ -162,7 +173,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<[BigNumber[], string]> {
return this._wrapCall(
super.sampleSellsFromLiquidityProviderRegistry,
this._handlers.sampleSellsFromLiquidityProviderRegistry,
@@ -179,7 +190,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
intermediateToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromMultiBridge,
this._handlers.sampleSellsFromMultiBridge,
@@ -195,7 +206,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromEth2Dai,
this._handlers.sampleBuysFromEth2Dai,
@@ -209,7 +220,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromUniswap,
this._handlers.sampleBuysFromUniswap,
@@ -219,10 +230,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
);
}
public sampleBuysFromUniswapV2(
path: string[],
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromUniswapV2,
this._handlers.sampleBuysFromUniswapV2,
@@ -241,7 +249,12 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
if (handler && this.getSelector(name) === selector) {
const args = this.getABIDecodedTransactionData<any>(name, callData);
const result = (handler as any)(...args);
return this._lookupAbiEncoder(this.getFunctionSignature(name)).encodeReturnValues([result]);
const encoder = this._lookupAbiEncoder(this.getFunctionSignature(name));
if (encoder.getReturnValueDataItem().components!.length === 1) {
return encoder.encodeReturnValues([result]);
} else {
return encoder.encodeReturnValues(result);
}
}
}
if (selector === this.getSelector('batchCall')) {
@@ -260,7 +273,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
): ContractFunctionObj<TResult> {
return {
...superFn.call(this, ...args),
callAsync: async (...callArgs: any[]): Promise<TResult> => {
callAsync: async (..._callArgs: any[]): Promise<TResult> => {
if (!handler) {
throw new Error(`${superFn.name} handler undefined`);
}

View File

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

View File

@@ -4,17 +4,16 @@
* -----------------------------------------------------------------------------
*/
export * from '../test/generated-wrappers/approximate_buys';
export * from '../test/generated-wrappers/balancer_sampler';
export * from '../test/generated-wrappers/curve_sampler';
export * from '../test/generated-wrappers/dummy_liquidity_provider';
export * from '../test/generated-wrappers/dummy_liquidity_provider_registry';
export * from '../test/generated-wrappers/erc20_bridge_sampler';
export * from '../test/generated-wrappers/eth2_dai_sampler';
export * from '../test/generated-wrappers/i_balancer';
export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_eth2_dai';
export * from '../test/generated-wrappers/i_kyber_hint_handler';
export * from '../test/generated-wrappers/i_kyber_network';
export * from '../test/generated-wrappers/i_kyber_network_proxy';
export * from '../test/generated-wrappers/i_kyber_storage';
export * from '../test/generated-wrappers/i_liquidity_provider';
export * from '../test/generated-wrappers/i_liquidity_provider_registry';
export * from '../test/generated-wrappers/i_m_stable';
@@ -31,5 +30,6 @@ export * from '../test/generated-wrappers/native_order_sampler';
export * from '../test/generated-wrappers/sampler_utils';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler';
export * from '../test/generated-wrappers/test_native_order_sampler';
export * from '../test/generated-wrappers/two_hop_sampler';
export * from '../test/generated-wrappers/uniswap_sampler';
export * from '../test/generated-wrappers/uniswap_v2_sampler';

View File

@@ -9,17 +9,16 @@
"generated-artifacts/ILiquidityProvider.json",
"generated-artifacts/ILiquidityProviderRegistry.json",
"test/generated-artifacts/ApproximateBuys.json",
"test/generated-artifacts/BalancerSampler.json",
"test/generated-artifacts/CurveSampler.json",
"test/generated-artifacts/DummyLiquidityProvider.json",
"test/generated-artifacts/DummyLiquidityProviderRegistry.json",
"test/generated-artifacts/ERC20BridgeSampler.json",
"test/generated-artifacts/Eth2DaiSampler.json",
"test/generated-artifacts/IBalancer.json",
"test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IEth2Dai.json",
"test/generated-artifacts/IKyberHintHandler.json",
"test/generated-artifacts/IKyberNetwork.json",
"test/generated-artifacts/IKyberNetworkProxy.json",
"test/generated-artifacts/IKyberStorage.json",
"test/generated-artifacts/ILiquidityProvider.json",
"test/generated-artifacts/ILiquidityProviderRegistry.json",
"test/generated-artifacts/IMStable.json",
@@ -36,6 +35,7 @@
"test/generated-artifacts/SamplerUtils.json",
"test/generated-artifacts/TestERC20BridgeSampler.json",
"test/generated-artifacts/TestNativeOrderSampler.json",
"test/generated-artifacts/TwoHopSampler.json",
"test/generated-artifacts/UniswapSampler.json",
"test/generated-artifacts/UniswapV2Sampler.json"
]

View File

@@ -37,6 +37,10 @@
{
"note": "Redeploy `MooniswapBridge` on Mainnet",
"pr": 2681
},
{
"note": "Redeploy `KyberBridge` on Mainnet",
"pr": 2683
}
]
},

View File

@@ -22,7 +22,7 @@
"uniswapBridge": "0x36691c4f426eb8f42f150ebde43069a31cb080ad",
"uniswapV2Bridge": "0xdcd6011f4c6b80e470d9487f5871a0cba7c93f48",
"erc20BridgeSampler": "0xd8c38704c9937ea3312de29f824b4ad3450a5e61",
"kyberBridge": "0x1c29670f7a77f1052d30813a0a4f632c78a02610",
"kyberBridge": "0xadd97271402590564ddd8ad23cb5317b1fb0fffb",
"eth2DaiBridge": "0x991c745401d5b5e469b8c3e2cb02c748f08754f1",
"chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438",
"dydxBridge": "0x92af95e37afddac412e5688a9dcc1dd815d4ae53",

View File

@@ -9,6 +9,10 @@
{
"note": "Update `ERC20BridgeSampler` artifact",
"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"
},
"config": {
"abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json"
"abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json"
},
"gitpkg": {
"registry": "git@github.com:0xProject/gitpkg-registry.git"

View File

@@ -11,5 +11,5 @@ ENV SNAPSHOT_HOST "http://ganache-snapshots.0x.org.s3-website.us-east-2.amazonaw
ENV SNAPSHOT_NAME "0x_ganache_snapshot"
EXPOSE 8545
CMD [ "sh", "-c", "echo downloading snapshot version: $VERSION; wget $SNAPSHOT_HOST/$SNAPSHOT_NAME-$VERSION.zip -O snapshot.zip && unzip -o snapshot.zip && ganache-cli --gasLimit 10000000 --db $SNAPSHOT_NAME --noVMErrorsOnRPCResponse -p 8545 --keepAliveTimeout=40000 --networkId \"$NETWORK_ID\" -m \"$MNEMONIC\" -h 0.0.0.0"]
CMD [ "sh", "-c", "echo downloading snapshot version: $VERSION; wget $SNAPSHOT_HOST/$SNAPSHOT_NAME-$VERSION.zip -O snapshot.zip && unzip -o snapshot.zip && ganache-cli --gasLimit 12000000 --allowUnlimitedContractSize=true --db $SNAPSHOT_NAME --noVMErrorsOnRPCResponse -p 8545 --keepAliveTimeout=40000 --networkId \"$NETWORK_ID\" -m \"$MNEMONIC\" -h 0.0.0.0"]

View File

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