Merge pull request #1980 from 0xProject/feature/asset-swapper/add-test-coverage

Add test coverage for asset-swapper
This commit is contained in:
David Sun
2019-07-29 11:20:36 -07:00
committed by GitHub
12 changed files with 1160 additions and 166 deletions

View File

@@ -58,7 +58,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
const { takerAssetFillAmount } = params;
args = [orders, takerAssetFillAmount, signatures];
}
const calldataHexString = abiEncoder.encode(args);
const calldataHexString = abiEncoder.encode(args, { shouldOptimize: true });
return {
calldataHexString,
methodAbi,

View File

@@ -56,7 +56,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
const { to, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts);
const abiEncoder = new AbiEncoder.Method(methodAbi);
const { orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient } = params;
let args: any[];
@@ -66,7 +65,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
} else {
args = [orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient];
}
const calldataHexString = abiEncoder.encode(args);
const calldataHexString = abiEncoder.encode(args, { shouldOptimize: true });
return {
calldataHexString,
methodAbi,

View File

@@ -75,19 +75,19 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts>,
): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
if (opts.useConsumerType === ConsumerType.Exchange) {
return this._exchangeConsumer;
} else if (opts.useConsumerType === ConsumerType.Forwarder) {
return this._forwarderConsumer;
} else {
return swapQuoteConsumerUtils.getConsumerForSwapQuoteAsync(
const useConsumerType =
opts.useConsumerType ||
(await swapQuoteConsumerUtils.getConsumerTypeForSwapQuoteAsync(
quote,
this._contractWrappers,
this.provider,
this._exchangeConsumer,
this._forwarderConsumer,
opts,
);
));
if (useConsumerType === ConsumerType.Exchange) {
return this._exchangeConsumer;
} else if (useConsumerType === ConsumerType.Forwarder) {
return this._forwarderConsumer;
}
return this._exchangeConsumer;
}
}

View File

@@ -35,11 +35,15 @@ export const assert = {
_.every(orders, (order: SignedOrder, index: number) => {
assert.assert(
order.takerAssetData === takerAssetData,
`Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData}`,
`Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${
order.takerAssetData
}`,
);
assert.assert(
order.makerAssetData === makerAssetData,
`Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData}`,
`Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${
order.makerAssetData
}`,
);
});
},

View File

@@ -220,7 +220,7 @@ function calculateQuoteInfo(
let takerTokenAmount = marketOperation === MarketOperation.Sell ? tokenAmount : constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
if (!shouldDisableFeeOrderCalculations && isMakerAssetZrxToken) {
if (isMakerAssetZrxToken) {
if (marketOperation === MarketOperation.Buy) {
takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makerTokenAmount);
} else {
@@ -229,7 +229,7 @@ function calculateQuoteInfo(
takerTokenAmount,
);
}
} else if (!shouldDisableFeeOrderCalculations) {
} else {
const findTokenAndZrxAmount =
marketOperation === MarketOperation.Buy
? findTakerTokenAndZrxAmountNeededToBuyAsset
@@ -246,7 +246,9 @@ function calculateQuoteInfo(
}
const zrxAmountToBuyAsset = tokenAndZrxAmountToBuyAsset[1];
// find eth amount needed to buy zrx
zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
zrxTakerTokenAmount = shouldDisableFeeOrderCalculations
? constants.ZERO_AMOUNT
: findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
}
const feeTakerTokenAmount = zrxTakerTokenAmount;

View File

@@ -6,12 +6,9 @@ import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { ExchangeSwapQuoteConsumer } from '../quote_consumers/exchange_swap_quote_consumer';
import { ForwarderSwapQuoteConsumer } from '../quote_consumers/forwarder_swap_quote_consumer';
import {
SmartContractParams,
ConsumerType,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerError,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
@@ -83,14 +80,12 @@ export const swapQuoteConsumerUtils = {
return optimizedOrder;
});
},
async getConsumerForSwapQuoteAsync(
async getConsumerTypeForSwapQuoteAsync(
quote: SwapQuote,
contractWrappers: ContractWrappers,
provider: Provider,
exchangeConsumer: ExchangeSwapQuoteConsumer,
forwarderConsumer: ForwarderSwapQuoteConsumer,
opts: Partial<SwapQuoteGetOutputOpts>,
): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
): Promise<ConsumerType> {
const wethAssetData = assetDataUtils.getEtherTokenAssetData(contractWrappers);
if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) {
if (opts.takerAddress !== undefined) {
@@ -108,14 +103,14 @@ export const swapQuoteConsumerUtils = {
);
if (isEnoughEthAndWethBalance[1]) {
// should be more gas efficient to use exchange consumer, so if possible use it.
return exchangeConsumer;
return ConsumerType.Exchange;
} else if (isEnoughEthAndWethBalance[0] && !isEnoughEthAndWethBalance[1]) {
return forwarderConsumer;
return ConsumerType.Forwarder;
}
// Note: defaulting to forwarderConsumer if takerAddress is null or not enough balance of either wEth or Eth
return forwarderConsumer;
return ConsumerType.Forwarder;
} else {
return exchangeConsumer;
return ConsumerType.Exchange;
}
},
};

View File

@@ -0,0 +1,92 @@
import { MarketOperation } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { affiliateFeeUtils } from '../src/utils/affiliate_fee_utils';
import { chaiSetup } from './utils/chai_setup';
import {
getFullyFillableSwapQuoteWithFees,
getFullyFillableSwapQuoteWithNoFees,
getPartialSignedOrdersWithFees,
getPartialSignedOrdersWithNoFees,
} from './utils/swap_quote';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_TAKER_ASSET_DATA = '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48';
const FAKE_MAKER_ASSET_DATA = '0xf47261b00000000000000000000000009f5B0C7e1623793bF0620569b9749e79DF6D0bC5';
const NULL_ADDRESS = constants.NULL_ADDRESS;
const FEE_PERCENTAGE = 0.1;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)];
const FILLABLE_FEE_AMOUNTS = [new BigNumber(1), new BigNumber(1), new BigNumber(1)];
const MARKET_OPERATION = MarketOperation.Sell;
describe('affiliateFeeUtils', () => {
const fakeFeeOrders = getPartialSignedOrdersWithNoFees(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
NULL_ADDRESS,
NULL_ADDRESS,
FILLABLE_FEE_AMOUNTS,
);
const fakeOrders = getPartialSignedOrdersWithNoFees(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
NULL_ADDRESS,
NULL_ADDRESS,
FILLABLE_AMOUNTS,
);
const fakeOrdersWithFees = getPartialSignedOrdersWithFees(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
NULL_ADDRESS,
NULL_ADDRESS,
FILLABLE_AMOUNTS,
FILLABLE_FEE_AMOUNTS,
);
const fakeSwapQuote = getFullyFillableSwapQuoteWithNoFees(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
fakeOrders,
MARKET_OPERATION,
);
const fakeSwapQuoteWithFees = getFullyFillableSwapQuoteWithFees(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
fakeOrdersWithFees,
fakeFeeOrders,
MARKET_OPERATION,
);
describe('getSwapQuoteWithAffiliateFee', () => {
it('should return unchanged swapQuote if feePercentage is 0', () => {
const updatedSwapQuote = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(fakeSwapQuote, 0);
const fakeSwapQuoteWithAffiliateFees = { ...fakeSwapQuote, ...{ feePercentage: 0 } };
expect(updatedSwapQuote).to.deep.equal(fakeSwapQuoteWithAffiliateFees);
});
it('should return correct feeTakerToken and totalTakerToken amounts when provided SwapQuote with no fees', () => {
const updatedSwapQuote = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(fakeSwapQuote, FEE_PERCENTAGE);
expect(updatedSwapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.deep.equal(new BigNumber(1));
expect(updatedSwapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.deep.equal(new BigNumber(11));
expect(updatedSwapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.deep.equal(new BigNumber(1));
expect(updatedSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.deep.equal(new BigNumber(11));
});
it('should return correct feeTakerToken and totalTakerToken amounts when provides SwapQuote with fees', () => {
const updatedSwapQuote = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(
fakeSwapQuoteWithFees,
FEE_PERCENTAGE,
);
expect(updatedSwapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.deep.equal(new BigNumber(4));
expect(updatedSwapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.deep.equal(new BigNumber(14));
expect(updatedSwapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.deep.equal(new BigNumber(4));
expect(updatedSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.deep.equal(new BigNumber(14));
});
});
});

View File

@@ -0,0 +1,230 @@
import { ContractAddresses, ContractWrappers } from '@0x/contract-wrappers';
import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { FillScenarios } from '@0x/fill-scenarios';
import { assetDataUtils } from '@0x/order-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote } from '../src';
import { constants } from '../src/constants';
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
import {
ExchangeMarketBuySmartContractParams,
ExchangeMarketSellSmartContractParams,
MarketBuySwapQuote,
MarketSellSwapQuote,
} from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_NETWORK_ID = 50;
const FILLABLE_AMOUNTS = [new BigNumber(3), new BigNumber(2), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
describe('ExchangeSwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let fillScenarios: FillScenarios;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
const networkId = TESTRPC_NETWORK_ID;
let orders: SignedOrder[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ExchangeSwapQuoteConsumer;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
fillScenarios = new FillScenarios(
provider,
userAddresses,
contractAddresses.zrxToken,
contractAddresses.exchange,
contractAddresses.erc20Proxy,
contractAddresses.erc721Proxy,
);
const config = {
networkId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
orders = [];
for (const fillableAmmount of FILLABLE_AMOUNTS) {
const order = await fillScenarios.createFillableSignedOrderAsync(
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
fillableAmmount,
);
orders.push(order);
}
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
orders,
MarketOperation.Sell,
);
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
orders,
MarketOperation.Buy,
);
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, {
networkId,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('executeSwapQuoteOrThrowAsync', () => {
/*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress });
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress });
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('getSmartContractParamsOrThrow', () => {
describe('valid swap quote', async () => {
// TODO(david) Check for valid MethodAbi
it('provide correct and optimized smart contract params for a marketSell SwapQuote', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.exchange.address);
const { takerAssetFillAmount, signatures, type } = params as ExchangeMarketSellSmartContractParams;
expect(type).to.deep.equal(MarketOperation.Sell);
expect(takerAssetFillAmount).to.bignumber.equal(
(marketSellSwapQuote as MarketSellSwapQuote).takerAssetFillAmount,
);
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
});
it('provide correct and optimized smart contract params for a marketBuy SwapQuote', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.exchange.address);
const { makerAssetFillAmount, signatures, type } = params as ExchangeMarketBuySmartContractParams;
expect(type).to.deep.equal(MarketOperation.Buy);
expect(makerAssetFillAmount).to.bignumber.equal(
(marketBuySwapQuote as MarketBuySwapQuote).makerAssetFillAmount,
);
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
});
});
});
describe('getCalldataOrThrow', () => {
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, to } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.exchange.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to,
data: calldataHexString,
gas: 4000000,
});
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, to } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.exchange.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to,
data: calldataHexString,
gas: 4000000,
});
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
});
});
});

View File

@@ -1,136 +1,416 @@
// import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils';
// import { BlockchainLifecycle } from '@0x/dev-utils';
// import { FillScenarios } from '@0x/fill-scenarios';
// import { assetDataUtils } from '@0x/order-utils';
// import { BigNumber } from '@0x/utils';
// import 'mocha';
import { ContractAddresses, ContractWrappers } from '@0x/contract-wrappers';
import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
// import { ForwarderSwapQuoteConsumer } from '../src';
import { SwapQuote } from '../src';
import { constants } from '../src/constants';
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
import {
ForwarderMarketBuySmartContractParams,
ForwarderMarketSellSmartContractParams,
MarketBuySwapQuote,
} from '../src/types';
// import { chaiSetup } from './utils/chai_setup';
// import { migrateOnceAsync } from './utils/migrate';
// import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFees } from './utils/swap_quote';
// import { provider, web3Wrapper } from './utils/web3_wrapper';
import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
// chaiSetup.configure();
// const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// const FILLABLE_AMOUNTS = [new BigNumber(5), new BigNumber(10)];
// const TESTRPC_NETWORK_ID = 50;
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_NETWORK_ID = 50;
const MARKET_OPERATION = MarketOperation.Sell;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
// describe('ForwarderSwapQuoteConsumer', () => {
// // let userAddresses: string[];
// // let makerAddress: string;
// // let takerAddress: string;
// // let fillScenarios: FillScenarios;
// // let feeRecipient: string;
// // let makerAssetData: string;
// // let takerAssetData: string;
// // let wethAssetData: string;
// // const networkId = TESTRPC_NETWORK_ID;
// before(async () => {
// // const contractAddresses = await migrateOnceAsync();
// // await blockchainLifecycle.startAsync();
// // userAddresses = await web3Wrapper.getAvailableAddressesAsync();
// // fillScenarios = new FillScenarios(
// // provider,
// // userAddresses,
// // contractAddresses.zrxToken,
// // contractAddresses.exchange,
// // contractAddresses.erc20Proxy,
// // contractAddresses.erc721Proxy,
// // );
// // [makerAddress, takerAddress, feeRecipient] = userAddresses;
// // const [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
// // [makerAssetData, takerAssetData, wethAssetData] = [
// // assetDataUtils.encodeERC20AssetData(makerTokenAddress),
// // assetDataUtils.encodeERC20AssetData(takerTokenAddress),
// // assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
// // ];
// });
// after(async () => {
// // await blockchainLifecycle.revertAsync();
// });
// beforeEach(async () => {
// // await blockchainLifecycle.startAsync();
// // This constructor has incorrect types
// });
// afterEach(async () => {
// // await blockchainLifecycle.revertAsync();
// });
// describe('getSmartContractParamsOrThrow', () => {
describe('ForwarderSwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
// describe('validation', () => {
// it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
// // const invalidSignedOrders = getSignedOrdersWithNoFees(
// // makerAssetData,
// // takerAssetData,
// // makerAddress,
// // takerAddress,
// // FILLABLE_AMOUNTS,
// // );
// // const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(makerAssetData, takerAssetData, invalidSignedOrders);
// // const swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, {});
// // TODO(dave4506) finish up testing/coverage
// // expect(
// // swapQuoteConsumer.getSmartContractParamsOrThrow(invalidSwapQuote, {}),
// // ).to.throws();
// });
// });
let orders: SignedOrder[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ForwarderSwapQuoteConsumer;
let erc20ProxyAddress: string;
// describe('valid swap quote', async () => {
// it('provide correct smart contract params with default options', async () => {
// // const signedOrders = getSignedOrdersWithNoFees(
// // makerAssetData,
// // wethAssetData,
// // makerAddress,
// // takerAddress,
// // FILLABLE_AMOUNTS,
// // );
// // const swapQuote = getFullyFillableSwapQuoteWithNoFees(makerAssetData, takerAssetData, signedOrders);
// // const swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, { networkId });
// // const smartContractParamsInfo = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(swapQuote, {});
// // console.log(smartContractParamsInfo);
// // TODO(dave4506): Add elaborate testing
// });
// });
// });
const networkId = TESTRPC_NETWORK_ID;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
networkId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
const UNLIMITED_ALLOWANCE = contractWrappers.erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
erc20ProxyAddress = contractWrappers.erc20Proxy.address;
// describe('getCalldataOrThrow', () => {
const totalFillableAmount = FILLABLE_AMOUNTS.reduce(
(a: BigNumber, c: BigNumber) => a.plus(c),
new BigNumber(0),
);
// describe('validation', () => {
// it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
// // const invalidSignedOrders = getSignedOrdersWithNoFees(
// // makerAssetData,
// // takerAssetData,
// // makerAddress,
// // takerAddress,
// // FILLABLE_AMOUNTS,
// // );
// // const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(makerAssetData, takerAssetData, invalidSignedOrders);
// // const swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, {});
// // TODO(dave4506) finish up testing/coverage
// // expect(
// // swapQuoteConsumer.getSmartContractParamsOrThrow(invalidSwapQuote, {}),
// // ).to.throws();
// });
// });
await contractWrappers.erc20Token.transferAsync(
makerTokenAddress,
coinbaseAddress,
makerAddress,
totalFillableAmount,
);
// describe('valid swap quote', async () => {
// it('provide correct calldata hex with default options', async () => {
// // const signedOrders = getSignedOrdersWithNoFees(
// // makerAssetData,
// // wethAssetData,
// // makerAddress,
// // takerAddress,
// // FILLABLE_AMOUNTS,
// // );
// // const swapQuote = getFullyFillableSwapQuoteWithNoFees(makerAssetData, takerAssetData, signedOrders);
// // const swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, { networkId });
// // const callDataInfo = await swapQuoteConsumer.getCalldataOrThrowAsync(swapQuote, {});
// // console.log(callDataInfo);
// // TODO(dave4506): Add elaborate testing
// });
// });
// });
// });
await contractWrappers.erc20Token.setAllowanceAsync(
makerTokenAddress,
makerAddress,
erc20ProxyAddress,
UNLIMITED_ALLOWANCE,
);
orders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
contractAddresses.exchange,
);
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Sell,
);
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Buy,
);
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, {
networkId,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('executeSwapQuoteOrThrowAsync', () => {
describe('validation', () => {
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
expect(
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidSwapQuote, { takerAddress }),
).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
});
// TODO(david) test execution of swap quotes with fee orders
describe('valid swap quote', () => {
/*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress });
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI));
});
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress });
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
it('should perform a marketBuy execution with affiliate fees', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
feePercentage: 0.05,
feeRecipient,
});
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI),
);
});
// TODO(david) Finish marketSell affiliate fee excution testing
// it('should perform a marketSell execution with affiliate fees', async () => {
// let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
// let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
// const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
// expect(makerBalance).to.bignumber.equal((new BigNumber(10)).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceBefore);
// await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, feePercentage: 0.05, feeRecipient });
// makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
// takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
// const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceAfter);
// expect(makerBalance).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal((new BigNumber(9.5)).multipliedBy(ONE_ETH_IN_WEI));
// expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI));
// });
});
});
describe('getSmartContractParamsOrThrow', () => {
describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
expect(swapQuoteConsumer.getSmartContractParamsOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
});
describe('valid swap quote', async () => {
it('provide correct and optimized smart contract params with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.forwarder.address);
const {
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
type,
} = params as ForwarderMarketSellSmartContractParams;
expect(type).to.deep.equal(MarketOperation.Sell);
expect(feeRecipientFromParams).to.deep.equal(constants.NULL_ADDRESS);
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0);
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.forwarder.address);
const {
makerAssetFillAmount,
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
type,
} = params as ForwarderMarketBuySmartContractParams;
expect(type).to.deep.equal(MarketOperation.Buy);
expect(feeRecipientFromParams).to.deep.equal(constants.NULL_ADDRESS);
expect(makerAssetFillAmount).to.bignumber.equal(
(marketBuySwapQuote as MarketBuySwapQuote).makerAssetFillAmount,
);
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0);
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with affiliate fees for a marketSell SwapQuote', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
feePercentage: 0.05,
feeRecipient,
});
expect(to).to.deep.equal(contractWrappers.forwarder.address);
const {
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
type,
} = params as ForwarderMarketSellSmartContractParams;
expect(type).to.deep.equal(MarketOperation.Sell);
expect(feeRecipientFromParams).to.deep.equal(feeRecipient);
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI));
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with affiliate fees for a marketBuy SwapQuote', async () => {
const { to, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketBuySwapQuote, {
feePercentage: 0.05,
feeRecipient,
});
expect(to).to.deep.equal(contractWrappers.forwarder.address);
const {
makerAssetFillAmount,
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
type,
} = params as ForwarderMarketBuySmartContractParams;
expect(type).to.deep.equal(MarketOperation.Buy);
expect(feeRecipientFromParams).to.deep.equal(feeRecipient);
expect(makerAssetFillAmount).to.bignumber.equal(
(marketBuySwapQuote as MarketBuySwapQuote).makerAssetFillAmount,
);
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI));
expect(feeSignatures).to.deep.equal([]);
});
});
});
describe('getCalldataOrThrow', () => {
describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
});
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, to } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(to).to.deep.equal(contractWrappers.forwarder.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to,
data: calldataHexString,
value: marketSellSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount,
gas: 4000000,
});
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI));
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, to } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(to).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to,
data: calldataHexString,
value: marketBuySwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount,
gas: 4000000,
});
makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
});
// TODO(david) finish testing for affiliate fees calldata output
// it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => {
// });
// it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => {
// });
});
});
});

View File

@@ -5,6 +5,7 @@ import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { constants } from '../src/constants';
import { OrdersAndFillableAmounts, SwapQuoterError } from '../src/types';
import { swapQuoteCalculator } from '../src/utils/swap_quote_calculator';
@@ -70,7 +71,7 @@ describe('swapQuoteCalculator', () => {
});
describe('InsufficientLiquidityError', () => {
it('should throw if not enough taker asset liquidity (multiple orders)', () => {
// we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 takerAsset units
// we have 150 takerAsset units available to sell but attempt to calculate a quote for 200 takerAsset units
const errorFunction = () => {
swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
@@ -84,7 +85,7 @@ describe('swapQuoteCalculator', () => {
testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(150));
});
it('should throw if not enough taker asset liquidity (multiple orders with 20% slippage)', () => {
// we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 makerAsset units
// we have 150 takerAsset units available to sell but attempt to calculate a quote for 200 takerAsset units
const errorFunction = () => {
swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
@@ -98,7 +99,7 @@ describe('swapQuoteCalculator', () => {
testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(125));
});
it('should throw if not enough taker asset liquidity (multiple orders with 5% slippage)', () => {
// we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 makerAsset units
// we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 takerAsset units
const errorFunction = () => {
swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
@@ -201,12 +202,12 @@ describe('swapQuoteCalculator', () => {
).to.not.throw();
});
it('should throw if not enough ZRX liquidity', () => {
// we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available
// we request 75 takerAsset units but the ZRX order is only enough to fill the first order, which only has 50 takerAsset units available
expect(() =>
swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
smallFeeOrderAndFillableAmount,
new BigNumber(125),
new BigNumber(75),
0,
false,
false,
@@ -214,7 +215,7 @@ describe('swapQuoteCalculator', () => {
).to.throw(SwapQuoterError.InsufficientZrxLiquidity);
});
it('calculates a correct swapQuote with no slippage', () => {
// we request 100 takerAsset units which can be filled using the first order
// we request 50 takerAsset units which can be filled using the first order
// the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
const assetSellAmount = new BigNumber(50);
const slippagePercentage = 0;
@@ -230,7 +231,7 @@ describe('swapQuoteCalculator', () => {
expect(swapQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
expect(swapQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]);
// test if rates are correct
// 50 eth to fill the first order + 100 eth for fees
// 50 takerAsset units to fill the first order + 100 takerAsset units for fees
const expectedMakerAssetAmountForTakerAsset = new BigNumber(200);
const expectedTakerAssetAmountForZrxFees = new BigNumber(100);
const expectedTotalTakerAssetAmount = assetSellAmount.plus(expectedTakerAssetAmountForZrxFees);
@@ -258,7 +259,7 @@ describe('swapQuoteCalculator', () => {
// we request 50 takerAsset units which can be filled using the first order
// however with 50% slippage we are protecting the buy with 25 extra takerAssetUnits
// so we need enough orders to fill 75 takerAssetUnits
// 150 takerAssetUnits can only be filled using both orders
// 75 takerAssetUnits can only be filled using both orders
// the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
const assetSellAmount = new BigNumber(50);
const slippagePercentage = 0.5;
@@ -274,7 +275,6 @@ describe('swapQuoteCalculator', () => {
expect(swapQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders);
expect(swapQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders);
// test if rates are correct
// 50 eth to fill the first order + 100 eth for fees
const expectedMakerAssetAmountForTakerAsset = new BigNumber(200);
const expectedTakerAssetAmountForZrxFees = new BigNumber(100);
const expectedTotalTakerAssetAmount = assetSellAmount.plus(expectedTakerAssetAmountForZrxFees);
@@ -286,7 +286,7 @@ describe('swapQuoteCalculator', () => {
expectedTakerAssetAmountForZrxFees,
);
expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount);
// 100 eth to fill the first order + 208 eth for fees
const expectedWorstMakerAssetAmountForTakerAsset = new BigNumber(100);
const expectedWorstTakerAssetAmountForZrxFees = new BigNumber(99);
const expectedWorstTotalTakerAssetAmount = assetSellAmount.plus(expectedWorstTakerAssetAmountForZrxFees);
@@ -301,6 +301,80 @@ describe('swapQuoteCalculator', () => {
expectedWorstTotalTakerAssetAmount,
);
});
it('calculates a correct swapQuote (with fee calculations disabled) with no slippage', () => {
// we request 50 takerAsset units which can be filled using the first order
const assetSellAmount = new BigNumber(50);
const slippagePercentage = 0;
const swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
smallFeeOrderAndFillableAmount,
assetSellAmount,
slippagePercentage,
false,
true,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
expect(swapQuote.feeOrders).to.deep.equal([]);
// test if rates are correct
const expectedMakerAssetAmountForTakerAsset = new BigNumber(200);
const expectedTotalTakerAssetAmount = assetSellAmount;
expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(assetSellAmount);
expect(swapQuote.bestCaseQuoteInfo.makerTokenAmount).to.bignumber.equal(
expectedMakerAssetAmountForTakerAsset,
);
expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount);
// because we have no slippage protection, minRate is equal to maxRate
expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(assetSellAmount);
expect(swapQuote.worstCaseQuoteInfo.makerTokenAmount).to.bignumber.equal(
expectedMakerAssetAmountForTakerAsset,
);
expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(
expectedTotalTakerAssetAmount,
);
});
it('calculates a correct swapQuote (with fee calculatations disabled) with slippage', () => {
// we request 50 takerAsset units which can be filled using the first order
// however with 50% slippage we are protecting the buy with 25 extra takerAssetUnits
// so we need enough orders to fill 75 takerAssetUnits
// 50 takerAssetUnits can only be filled using both orders
// the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
const assetSellAmount = new BigNumber(50);
const slippagePercentage = 0.5;
const swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
allFeeOrdersAndFillableAmounts,
assetSellAmount,
slippagePercentage,
false,
true,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders);
expect(swapQuote.feeOrders).to.deep.equal([]);
// test if rates are correct
const expectedMakerAssetAmountForTakerAsset = new BigNumber(200);
const expectedTotalTakerAssetAmount = assetSellAmount;
expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(assetSellAmount);
expect(swapQuote.bestCaseQuoteInfo.makerTokenAmount).to.bignumber.equal(
expectedMakerAssetAmountForTakerAsset,
);
expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount);
// 100 eth to fill the first order + 208 eth for fees
const expectedWorstMakerAssetAmountForTakerAsset = new BigNumber(100);
const expectedWorstTotalTakerAssetAmount = assetSellAmount;
expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(assetSellAmount);
expect(swapQuote.worstCaseQuoteInfo.makerTokenAmount).to.bignumber.equal(
expectedWorstMakerAssetAmountForTakerAsset,
);
expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(
expectedWorstTotalTakerAssetAmount,
);
});
});
describe('#calculateMarketBuySwapQuote', () => {
let firstOrder: SignedOrder;
@@ -588,5 +662,78 @@ describe('swapQuoteCalculator', () => {
expectedWorstTotalTakerAssetAmount,
);
});
it('calculates a correct swapQuote (with fee calculations disabled) with no slippage', () => {
// we request 200 makerAsset units which can be filled using the first order
// the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
const assetBuyAmount = new BigNumber(200);
const slippagePercentage = 0;
const swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote(
ordersAndFillableAmounts,
smallFeeOrderAndFillableAmount,
assetBuyAmount,
slippagePercentage,
false,
true,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
expect(swapQuote.feeOrders).to.deep.equal([]);
// test if rates are correct
// 50 eth to fill the first order + 100 eth for fees
const expectedTakerAssetAmountForMakerAsset = new BigNumber(50);
const expectedTotalTakerAssetAmount = expectedTakerAssetAmountForMakerAsset;
expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(
expectedTakerAssetAmountForMakerAsset,
);
expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount);
// because we have no slippage protection, minRate is equal to maxRate
expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(
expectedTakerAssetAmountForMakerAsset,
);
expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(
expectedTotalTakerAssetAmount,
);
});
it('calculates a correct swapQuote (with fee calculations disabled) with slippage', () => {
// we request 200 makerAsset units which can be filled using the first order
// however with 50% slippage we are protecting the buy with 100 extra makerAssetUnits
// so we need enough orders to fill 300 makerAssetUnits
// 300 makerAssetUnits can only be filled using both orders
// the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
const assetBuyAmount = new BigNumber(200);
const slippagePercentage = 0.5;
const swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote(
ordersAndFillableAmounts,
allFeeOrdersAndFillableAmounts,
assetBuyAmount,
slippagePercentage,
false,
true,
);
// test if orders are correct
expect(swapQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders);
expect(swapQuote.feeOrders).to.deep.equal([]);
// test if rates are correct
// 50 eth to fill the first order + 100 eth for fees
const expectedTakerAssetAmountForMakerAsset = new BigNumber(50);
const expectedTotalTakerAssetAmount = expectedTakerAssetAmountForMakerAsset;
expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(
expectedTakerAssetAmountForMakerAsset,
);
expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount);
// 100 eth to fill the first order + 208 eth for fees
const expectedWorstTakerAssetAmountForMakerAsset = new BigNumber(100);
const expectedWorstTotalTakerAssetAmount = expectedWorstTakerAssetAmountForMakerAsset;
expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal(
expectedWorstTakerAssetAmountForMakerAsset,
);
expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(
expectedWorstTotalTakerAssetAmount,
);
});
});
});

View File

@@ -0,0 +1,171 @@
import { ContractAddresses, ContractWrappers } from '@0x/contract-wrappers';
import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote } from '../src';
import { ConsumerType } from '../src/types';
import { swapQuoteConsumerUtils } from '../src/utils/swap_quote_consumer_utils';
import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_NETWORK_ID = 50;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
const LARGE_FILLABLE_AMOUNTS = [new BigNumber(20), new BigNumber(20), new BigNumber(20)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
describe('swapQuoteConsumerUtils', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[];
let makerAddress: string;
let takerAddress: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
const networkId = TESTRPC_NETWORK_ID;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
networkId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[takerAddress, makerAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('getConsumerTypeForSwapQuoteAsync', () => {
let forwarderOrders: SignedOrder[];
let exchangeOrders: SignedOrder[];
let largeForwarderOrders: SignedOrder[];
let forwarderSwapQuote: SwapQuote;
let exchangeSwapQuote: SwapQuote;
let largeForwarderSwapQuote: SwapQuote;
beforeEach(async () => {
exchangeOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
forwarderOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
largeForwarderOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
LARGE_FILLABLE_AMOUNTS,
);
forwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
forwarderOrders,
MarketOperation.Sell,
);
largeForwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
largeForwarderOrders,
MarketOperation.Sell,
);
exchangeSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
exchangeOrders,
MarketOperation.Sell,
);
});
it('should return exchange consumer if takerAsset is not wEth', async () => {
const consumerType = await swapQuoteConsumerUtils.getConsumerTypeForSwapQuoteAsync(
exchangeSwapQuote,
contractWrappers,
provider,
{ takerAddress },
);
expect(consumerType).to.equal(ConsumerType.Exchange);
});
it('should return forwarder consumer if takerAsset is wEth and have enough eth balance', async () => {
const consumerType = await swapQuoteConsumerUtils.getConsumerTypeForSwapQuoteAsync(
forwarderSwapQuote,
contractWrappers,
provider,
{ takerAddress },
);
expect(consumerType).to.equal(ConsumerType.Forwarder);
});
it('should return exchange consumer if takerAsset is wEth and taker has enough weth', async () => {
const etherInWei = new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI);
await contractWrappers.etherToken.depositAsync(contractAddresses.etherToken, etherInWei, takerAddress);
const consumerType = await swapQuoteConsumerUtils.getConsumerTypeForSwapQuoteAsync(
forwarderSwapQuote,
contractWrappers,
provider,
{ takerAddress },
);
expect(consumerType).to.equal(ConsumerType.Exchange);
});
it('should return forwarder consumer if takerAsset is wEth and takerAddress has no available balance in either weth or eth (defaulting behavior)', async () => {
const etherInWei = new BigNumber(50).multipliedBy(ONE_ETH_IN_WEI);
await contractWrappers.etherToken.depositAsync(contractAddresses.etherToken, etherInWei, takerAddress);
const consumerType = await swapQuoteConsumerUtils.getConsumerTypeForSwapQuoteAsync(
largeForwarderSwapQuote,
contractWrappers,
provider,
{ takerAddress },
);
expect(consumerType).to.equal(ConsumerType.Forwarder);
});
});
});

View File

@@ -1,13 +1,38 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { SupportedProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { constants } from '../../src/constants';
import { SwapQuote } from '../../src/types';
const ZERO_BIG_NUMBER = new BigNumber(0);
export const getSignedOrdersWithNoFees = (
export const getSignedOrdersWithNoFeesAsync = async (
provider: SupportedProvider,
makerAssetData: string,
takerAssetData: string,
makerAddress: string,
takerAddress: string,
fillableAmounts: BigNumber[],
exchangeAddress?: string,
): Promise<SignedOrder[]> => {
const promises = _.map(fillableAmounts, async (fillableAmount: BigNumber) =>
orderFactory.createSignedOrderAsync(
provider,
makerAddress,
fillableAmount,
makerAssetData,
fillableAmount,
takerAssetData,
exchangeAddress || constants.NULL_ADDRESS,
),
);
return Promise.all(promises);
};
export const getPartialSignedOrdersWithNoFees = (
makerAssetData: string,
takerAssetData: string,
makerAddress: string,
@@ -25,6 +50,55 @@ export const getSignedOrdersWithNoFees = (
);
};
export const getPartialSignedOrdersWithFees = (
makerAssetData: string,
takerAssetData: string,
makerAddress: string,
takerAddress: string,
fillableAmounts: BigNumber[],
takerFees: BigNumber[],
): SignedOrder[] => {
const orders = getPartialSignedOrdersWithNoFees(
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
fillableAmounts,
);
return _.map(orders, (order: SignedOrder, index: number) =>
orderFactory.createSignedOrderFromPartial({
...order,
...{ takerFee: takerFees[index] },
}),
);
};
export const getFullyFillableSwapQuoteWithFees = (
makerAssetData: string,
takerAssetData: string,
orders: SignedOrder[],
feeOrders: SignedOrder[],
operation: MarketOperation,
) => {
const swapQuote = getFullyFillableSwapQuoteWithNoFees(makerAssetData, takerAssetData, orders, operation);
swapQuote.feeOrders = feeOrders;
const totalFeeTakerTokenAmount = _.reduce(
feeOrders,
(a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount),
ZERO_BIG_NUMBER,
);
// Adds fees to the SwapQuoteInfos assuming all feeOrders will be filled
swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount = totalFeeTakerTokenAmount;
swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount = totalFeeTakerTokenAmount;
swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount = swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount.plus(
totalFeeTakerTokenAmount,
);
swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount = swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount.plus(
totalFeeTakerTokenAmount,
);
return swapQuote;
};
export const getFullyFillableSwapQuoteWithNoFees = (
makerAssetData: string,
takerAssetData: string,