@0x/asset-swapper: Rebase against development and pay protocol fees.

This commit is contained in:
Lawrence Forman 2019-11-18 09:37:02 -05:00
parent 32258ef666
commit 439c98a6e5
42 changed files with 5681 additions and 3174 deletions

View File

@ -1,4 +1,17 @@
[ [
{
"version": "2.1.0-beta.3",
"changes": [
{
"note": "Refactor of logic for marketBuy/marketSell order pruning and selecting, introduced protocol fees, and refactored types used by the package",
"pr": 2272
},
{
"note": "Incorporate paying protocol fees.",
"pr": 2350
}
]
},
{ {
"version": "2.1.0-beta.2", "version": "2.1.0-beta.2",
"changes": [ "changes": [
@ -7,7 +20,7 @@
"pr": 2342 "pr": 2342
} }
], ],
"timestamp": 1574030254 "timestamp": 1573159180
}, },
{ {
"version": "2.1.0-beta.1", "version": "2.1.0-beta.1",

View File

@ -5,10 +5,6 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG CHANGELOG
## v2.1.0-beta.2 - _November 17, 2019_
* Update BigNumber version to ~9.0.0 (#2342)
## v2.1.0-beta.1 - _November 7, 2019_ ## v2.1.0-beta.1 - _November 7, 2019_
* All references to network ID have been removed, and references to chain ID have been introduced instead (#2313) * All references to network ID have been removed, and references to chain ID have been introduced instead (#2313)

File diff suppressed because it is too large Load Diff

1839
packages/asset-swapper/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@0x/asset-swapper", "name": "@0x/asset-swapper",
"version": "2.1.0-beta.2", "version": "2.1.0-beta.1",
"engines": { "engines": {
"node": ">=6.12" "node": ">=6.12"
}, },
@ -40,28 +40,30 @@
}, },
"homepage": "https://0x.org/asset-swapper", "homepage": "https://0x.org/asset-swapper",
"dependencies": { "dependencies": {
"@0x/assert": "^2.2.0-beta.2", "@0x/assert": "^2.2.0-beta.1",
"@0x/connect": "^5.1.0-beta.2", "@0x/contracts-dev-utils": "^0.1.0-beta.1",
"@0x/contract-addresses": "^3.3.0-beta.3", "@0x/contracts-erc20": "^2.3.0-beta.1",
"@0x/contract-wrappers": "^12.2.0-beta.2", "@0x/contracts-exchange": "^2.2.0-beta.1",
"@0x/dev-utils": "^2.4.0-beta.2", "@0x/contracts-exchange-forwarder": "^3.1.0-beta.1",
"@0x/json-schemas": "^4.1.0-beta.2", "@0x/json-schemas": "^4.1.0-beta.1",
"@0x/migrations": "^4.4.0-beta.2", "@0x/order-utils": "^8.5.0-beta.1",
"@0x/order-utils": "^8.5.0-beta.2", "@0x/orderbook": "^0.1.0-beta.1",
"@0x/orderbook": "^0.1.0-beta.2", "@0x/types": "^2.5.0-beta.1",
"@0x/subproviders": "^5.1.0-beta.2", "@0x/utils": "^4.6.0-beta.1",
"@0x/types": "^2.5.0-beta.2", "@0x/web3-wrapper": "^6.1.0-beta.1",
"@0x/typescript-typings": "^4.4.0-beta.2", "ethereum-types": "^2.2.0-beta.1",
"@0x/utils": "^4.6.0-beta.2",
"@0x/web3-wrapper": "^6.1.0-beta.2",
"ethereum-types": "^2.2.0-beta.2",
"lodash": "^4.17.11" "lodash": "^4.17.11"
}, },
"devDependencies": { "devDependencies": {
"@0x/contract-addresses": "^3.3.0-beta.2",
"@0x/contracts-test-utils": "^3.2.0-beta.2", "@0x/contracts-test-utils": "^3.2.0-beta.2",
"@0x/dev-utils": "^2.4.0-beta.1",
"@0x/mesh-rpc-client": "^7.0.4-beta-0xv3", "@0x/mesh-rpc-client": "^7.0.4-beta-0xv3",
"@0x/migrations": "^4.4.0-beta.1",
"@0x/subproviders": "^5.1.0-beta.1",
"@0x/ts-doc-gen": "^0.0.22", "@0x/ts-doc-gen": "^0.0.22",
"@0x/tslint-config": "^3.1.0-beta.2", "@0x/tslint-config": "^3.1.0-beta.1",
"@0x/typescript-typings": "^4.4.0-beta.1",
"@types/lodash": "4.14.104", "@types/lodash": "4.14.104",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/node": "*", "@types/node": "*",

View File

@ -1,55 +1,68 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { import {
ForwarderSwapQuoteExecutionOpts, ForwarderExtensionContractOpts,
ForwarderSwapQuoteGetOutputOpts, OrderPrunerOpts,
OrdersAndFillableAmounts, OrderPrunerPermittedFeeTypes,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
SwapQuoterOpts, SwapQuoterOpts,
} from './types'; } from './types';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x'; const NULL_BYTES = '0x';
const NULL_ERC20_ASSET_DATA = '0xf47261b00000000000000000000000000000000000000000000000000000000000000000';
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const MAINNET_CHAIN_ID = 1; const MAINNET_CHAIN_ID = 1;
const ONE_SECOND_MS = 1000; const ONE_SECOND_MS = 1000;
const DEFAULT_PER_PAGE = 1000; const DEFAULT_PER_PAGE = 1000;
const PROTOCOL_FEE_MULTIPLIER = 150000;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
chainId: MAINNET_CHAIN_ID,
orderRefreshIntervalMs: 10000, // 10 seconds
expiryBufferMs: 120000, // 2 minutes expiryBufferMs: 120000, // 2 minutes
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]), // Default asset-swapper for CFL oriented fee types
}; };
const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: ForwarderSwapQuoteGetOutputOpts = { const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
...{
chainId: MAINNET_CHAIN_ID,
orderRefreshIntervalMs: 10000, // 10 seconds
},
...DEFAULT_ORDER_PRUNER_OPTS,
};
const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts = {
feePercentage: 0, feePercentage: 0,
feeRecipient: NULL_ADDRESS, feeRecipient: NULL_ADDRESS,
}; };
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: ForwarderSwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS; const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts &
ForwarderExtensionContractOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
shouldDisableRequestingFeeOrders: false,
slippagePercentage: 0.2, // 20% slippage protection, slippagePercentage: 0.2, // 20% slippage protection,
}; };
const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = {
orders: [] as SignedOrder[],
remainingFillableMakerAssetAmounts: [] as BigNumber[],
};
export const constants = { export const constants = {
ETH_GAS_STATION_API_BASE_URL,
NULL_BYTES, NULL_BYTES,
ZERO_AMOUNT: new BigNumber(0), ZERO_AMOUNT: new BigNumber(0),
NULL_ADDRESS, NULL_ADDRESS,
MAINNET_CHAIN_ID, MAINNET_CHAIN_ID,
DEFAULT_ORDER_PRUNER_OPTS,
ETHER_TOKEN_DECIMALS: 18, ETHER_TOKEN_DECIMALS: 18,
ONE_AMOUNT: new BigNumber(1), ONE_AMOUNT: new BigNumber(1),
MAX_AFFILIATE_FEE_PERCENTAGE: 0.05,
ONE_SECOND_MS, ONE_SECOND_MS,
DEFAULT_SWAP_QUOTER_OPTS, DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS, DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
EMPTY_ORDERS_AND_FILLABLE_AMOUNTS,
DEFAULT_PER_PAGE, DEFAULT_PER_PAGE,
PROTOCOL_FEE_MULTIPLIER,
NULL_ERC20_ASSET_DATA,
}; };

View File

@ -20,11 +20,13 @@ export {
ConstructorStateMutability, ConstructorStateMutability,
} from 'ethereum-types'; } from 'ethereum-types';
export { SignedOrder, AssetPairsItem, APIOrder, Asset } from '@0x/types'; export { SignedOrder, AssetPairsItem, APIOrder, Asset, Order } from '@0x/types';
export { BigNumber } from '@0x/utils'; export { BigNumber } from '@0x/utils';
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { SwapQuoter } from './swap_quoter'; export { SwapQuoter } from './swap_quoter';
export { protocolFeeUtils } from './utils/protocol_fee_utils';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { InsufficientAssetLiquidityError } from './errors'; export { InsufficientAssetLiquidityError } from './errors';
export { export {
@ -34,21 +36,16 @@ export {
SwapQuoteConsumerOpts, SwapQuoteConsumerOpts,
CalldataInfo, CalldataInfo,
ExtensionContractType, ExtensionContractType,
SwapQuoteConsumingOpts,
LiquidityForTakerMakerAssetDataPair,
SwapQuoteGetOutputOpts, SwapQuoteGetOutputOpts,
PrunedSignedOrder,
SwapQuoteExecutionOpts, SwapQuoteExecutionOpts,
SwapQuoteInfo, SwapQuoteInfo,
GetExtensionContractTypeOpts, GetExtensionContractTypeOpts,
SwapQuoteExecutionOptsBase,
SwapQuoteGetOutputOptsBase,
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
SmartContractParamsInfo, SmartContractParamsInfo,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketSellSwapQuote, MarketSellSwapQuote,
MarketBuySwapQuoteWithAffiliateFee,
MarketSellSwapQuoteWithAffiliateFee,
LiquidityForAssetData,
OrdersAndFillableAmounts,
SwapQuoteConsumerBase, SwapQuoteConsumerBase,
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
} from './types'; } from './types';

View File

@ -1,5 +1,5 @@
import { ContractError, ContractWrappers, ForwarderError } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { MarketOperation } from '@0x/types'; import { ExchangeContract } from '@0x/contracts-exchange';
import { AbiEncoder, providerUtils } from '@0x/utils'; import { AbiEncoder, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import { MethodAbi } from 'ethereum-types'; import { MethodAbi } from 'ethereum-types';
@ -9,13 +9,13 @@ import { constants } from '../constants';
import { import {
CalldataInfo, CalldataInfo,
ExchangeSmartContractParams, ExchangeSmartContractParams,
MarketOperation,
SmartContractParamsInfo, SmartContractParamsInfo,
SwapQuote, SwapQuote,
SwapQuoteConsumerBase, SwapQuoteConsumerBase,
SwapQuoteConsumerError,
SwapQuoteConsumerOpts, SwapQuoteConsumerOpts,
SwapQuoteExecutionOptsBase, SwapQuoteExecutionOpts,
SwapQuoteGetOutputOptsBase, SwapQuoteGetOutputOpts,
} from '../types'; } from '../types';
import { assert } from '../utils/assert'; import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
@ -25,23 +25,24 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
public readonly provider: ZeroExProvider; public readonly provider: ZeroExProvider;
public readonly chainId: number; public readonly chainId: number;
private readonly _contractWrappers: ContractWrappers; private readonly _exchangeContract: ExchangeContract;
constructor(supportedProvider: SupportedProvider, options: Partial<SwapQuoteConsumerOpts> = {}) { constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId); assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider); const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider; this.provider = provider;
this.chainId = chainId; this.chainId = chainId;
this._contractWrappers = new ContractWrappers(this.provider, { this._exchangeContract = new ExchangeContract(contractAddresses.exchange, supportedProvider);
chainId,
});
} }
public async getCalldataOrThrowAsync( public async getCalldataOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOptsBase>, opts: Partial<SwapQuoteGetOutputOpts>,
): Promise<CalldataInfo> { ): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
@ -69,7 +70,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
public async getSmartContractParamsOrThrowAsync( public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
_opts: Partial<SwapQuoteGetOutputOptsBase>, _opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<SmartContractParamsInfo<ExchangeSmartContractParams>> { ): Promise<SmartContractParamsInfo<ExchangeSmartContractParams>> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
@ -77,8 +78,6 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
const signatures = _.map(orders, o => o.signature); const signatures = _.map(orders, o => o.signature);
const optimizedOrders = swapQuoteConsumerUtils.optimizeOrdersForMarketExchangeOperation(orders, quote.type);
let params: ExchangeSmartContractParams; let params: ExchangeSmartContractParams;
let methodName: string; let methodName: string;
@ -86,45 +85,43 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
const { makerAssetFillAmount } = quote; const { makerAssetFillAmount } = quote;
params = { params = {
orders: optimizedOrders, orders,
signatures, signatures,
makerAssetFillAmount, makerAssetFillAmount,
type: MarketOperation.Buy, type: MarketOperation.Buy,
}; };
methodName = 'marketBuyOrders'; methodName = 'marketBuyOrdersFillOrKill';
} else { } else {
const { takerAssetFillAmount } = quote; const { takerAssetFillAmount } = quote;
params = { params = {
orders: optimizedOrders, orders,
signatures, signatures,
takerAssetFillAmount, takerAssetFillAmount,
type: MarketOperation.Sell, type: MarketOperation.Sell,
}; };
methodName = 'marketSellOrders'; methodName = 'marketSellOrdersFillOrKill';
} }
const methodAbi = utils.getMethodAbiFromContractAbi( const methodAbi = utils.getMethodAbiFromContractAbi(this._exchangeContract.abi, methodName) as MethodAbi;
this._contractWrappers.exchange.abi,
methodName,
) as MethodAbi;
return { return {
params, params,
toAddress: this._contractWrappers.exchange.address, toAddress: this._exchangeContract.address,
methodAbi, methodAbi,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInEthAmount,
}; };
} }
public async executeSwapQuoteOrThrowAsync( public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOptsBase>, opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> { ): Promise<string> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
const { takerAddress, gasLimit, gasPrice } = opts; const { takerAddress, gasLimit, gasPrice, ethAmount } = opts;
if (takerAddress !== undefined) { if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress); assert.isETHAddressHex('takerAddress', takerAddress);
@ -135,41 +132,38 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase<Exchange
if (gasPrice !== undefined) { if (gasPrice !== undefined) {
assert.isBigNumber('gasPrice', gasPrice); assert.isBigNumber('gasPrice', gasPrice);
} }
if (ethAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount);
}
const { orders } = quote; const { orders } = quote;
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
const value = ethAmount || quote.worstCaseQuoteInfo.protocolFeeInEthAmount;
try { let txHash: string;
let txHash: string; if (quote.type === MarketOperation.Buy) {
if (quote.type === MarketOperation.Buy) { const { makerAssetFillAmount } = quote;
const { makerAssetFillAmount } = quote; txHash = await this._exchangeContract
txHash = await this._contractWrappers.exchange .marketBuyOrdersFillOrKill(orders, makerAssetFillAmount, orders.map(o => o.signature))
.marketBuyOrdersNoThrow(orders, makerAssetFillAmount, orders.map(o => o.signature)) .sendTransactionAsync({
.sendTransactionAsync({ from: finalTakerAddress,
from: finalTakerAddress, gas: gasLimit,
gas: gasLimit, gasPrice,
gasPrice, value,
}); });
} else { } else {
const { takerAssetFillAmount } = quote; const { takerAssetFillAmount } = quote;
txHash = await this._contractWrappers.exchange txHash = await this._exchangeContract
.marketSellOrdersNoThrow(orders, takerAssetFillAmount, orders.map(o => o.signature)) .marketSellOrdersFillOrKill(orders, takerAssetFillAmount, orders.map(o => o.signature))
.sendTransactionAsync({ .sendTransactionAsync({
from: finalTakerAddress, from: finalTakerAddress,
gas: gasLimit, gas: gasLimit,
gasPrice, gasPrice,
}); value,
} });
return txHash;
} catch (err) {
if (_.includes(err.message, ContractError.SignatureRequestDenied)) {
throw new Error(SwapQuoteConsumerError.SignatureRequestDenied);
} else if (_.includes(err.message, ForwarderError.CompleteFillFailed)) {
throw new Error(SwapQuoteConsumerError.TransactionValueTooLow);
} else {
throw err;
}
} }
// TODO(dorothy-zbornak): Handle signature request denied
// (see contract-wrappers/decorators)
// and ExchangeRevertErrors.IncompleteFillError.
return txHash;
} }
} }

View File

@ -1,5 +1,6 @@
import { ContractError, ContractWrappers, ForwarderError } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { MarketOperation } from '@0x/types'; import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ForwarderContract } from '@0x/contracts-exchange-forwarder';
import { AbiEncoder, providerUtils } from '@0x/utils'; import { AbiEncoder, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import { MethodAbi } from 'ethereum-types'; import { MethodAbi } from 'ethereum-types';
@ -8,14 +9,15 @@ import * as _ from 'lodash';
import { constants } from '../constants'; import { constants } from '../constants';
import { import {
CalldataInfo, CalldataInfo,
ForwarderExtensionContractOpts,
ForwarderSmartContractParams, ForwarderSmartContractParams,
ForwarderSwapQuoteExecutionOpts, MarketOperation,
ForwarderSwapQuoteGetOutputOpts,
SmartContractParamsInfo, SmartContractParamsInfo,
SwapQuote, SwapQuote,
SwapQuoteConsumerBase, SwapQuoteConsumerBase,
SwapQuoteConsumerError,
SwapQuoteConsumerOpts, SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types'; } from '../types';
import { affiliateFeeUtils } from '../utils/affiliate_fee_utils'; import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
import { assert } from '../utils/assert'; import { assert } from '../utils/assert';
@ -26,18 +28,23 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
public readonly provider: ZeroExProvider; public readonly provider: ZeroExProvider;
public readonly chainId: number; public readonly chainId: number;
private readonly _contractWrappers: ContractWrappers; private readonly _contractAddresses: ContractAddresses;
private readonly _forwarder: ForwarderContract;
private readonly _devUtils: DevUtilsContract;
constructor(supportedProvider: SupportedProvider, options: Partial<SwapQuoteConsumerOpts> = {}) { constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId); assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider); const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider; this.provider = provider;
this.chainId = chainId; this.chainId = chainId;
this._contractWrappers = new ContractWrappers(this.provider, { this._contractAddresses = contractAddresses;
chainId, this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
}); this._devUtils = new DevUtilsContract(contractAddresses.devUtils, supportedProvider);
} }
/** /**
@ -47,21 +54,21 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
*/ */
public async getCalldataOrThrowAsync( public async getCalldataOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteGetOutputOpts>, opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
): Promise<CalldataInfo> { ): Promise<CalldataInfo> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync()); assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
const { toAddress, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts); const { toAddress, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts);
const abiEncoder = new AbiEncoder.Method(methodAbi); const abiEncoder = new AbiEncoder.Method(methodAbi);
const { orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient } = params; const { orders, signatures, feePercentage, feeRecipient } = params;
let args: any[]; let args: any[];
if (params.type === MarketOperation.Buy) { if (params.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = params; const { makerAssetFillAmount } = params;
args = [orders, makerAssetFillAmount, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient]; args = [orders, makerAssetFillAmount, signatures, feePercentage, feeRecipient];
} else { } else {
args = [orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient]; args = [orders, signatures, feePercentage, feeRecipient];
} }
const calldataHexString = abiEncoder.encode(args, { shouldOptimize: true }); const calldataHexString = abiEncoder.encode(args, { shouldOptimize: true });
return { return {
@ -79,47 +86,42 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
*/ */
public async getSmartContractParamsOrThrowAsync( public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteGetOutputOpts>, opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> { ): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync()); assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
const { ethAmount, feeRecipient, feePercentage: unFormattedFeePercentage } = _.merge( const { ethAmount: providedEthAmount, feeRecipient, feePercentage } = _.merge(
{}, {},
constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
opts, opts,
); );
assert.isValidPercentage('feePercentage', unFormattedFeePercentage); assert.isValidPercentage('feePercentage', feePercentage);
assert.isETHAddressHex('feeRecipient', feeRecipient); assert.isETHAddressHex('feeRecipient', feeRecipient);
if (ethAmount !== undefined) { if (providedEthAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount); assert.isBigNumber('ethAmount', providedEthAmount);
} }
const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, unFormattedFeePercentage); const { orders, worstCaseQuoteInfo } = quote;
const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee;
// lowercase input addresses // lowercase input addresses
const normalizedFeeRecipientAddress = feeRecipient.toLowerCase(); const normalizedFeeRecipientAddress = feeRecipient.toLowerCase();
const signatures = _.map(orders, o => o.signature); const signatures = _.map(orders, o => o.signature);
const feeSignatures = _.map(feeOrders, o => o.signature);
const feePercentage = utils.numberPercentageToEtherTokenAmountPercentage(unFormattedFeePercentage); const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage);
let params: ForwarderSmartContractParams; let params: ForwarderSmartContractParams;
let methodName: string; let methodName: string;
if (quoteWithAffiliateFee.type === MarketOperation.Buy) { if (quote.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quoteWithAffiliateFee; const { makerAssetFillAmount } = quote;
params = { params = {
orders, orders,
makerAssetFillAmount, makerAssetFillAmount,
signatures, signatures,
feeOrders, feePercentage: formattedFeePercentage,
feeSignatures,
feePercentage,
feeRecipient: normalizedFeeRecipientAddress, feeRecipient: normalizedFeeRecipientAddress,
type: MarketOperation.Buy, type: MarketOperation.Buy,
}; };
@ -129,23 +131,21 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
params = { params = {
orders, orders,
signatures, signatures,
feeOrders, feePercentage: formattedFeePercentage,
feeSignatures,
feePercentage,
feeRecipient: normalizedFeeRecipientAddress, feeRecipient: normalizedFeeRecipientAddress,
type: MarketOperation.Sell, type: MarketOperation.Sell,
}; };
methodName = 'marketSellOrdersWithEth'; methodName = 'marketSellOrdersWithEth';
} }
const methodAbi = utils.getMethodAbiFromContractAbi( const methodAbi = utils.getMethodAbiFromContractAbi(this._forwarder.abi, methodName) as MethodAbi;
this._contractWrappers.forwarder.abi,
methodName,
) as MethodAbi;
const ethAmountWithFees = affiliateFeeUtils
.getTotalEthAmountWithAffiliateFee(worstCaseQuoteInfo, feePercentage)
.plus(worstCaseQuoteInfo.protocolFeeInEthAmount);
return { return {
params, params,
toAddress: this._contractWrappers.forwarder.address, toAddress: this._forwarder.address,
ethAmount: ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount, ethAmount: providedEthAmount || ethAmountWithFees,
methodAbi, methodAbi,
}; };
} }
@ -157,11 +157,11 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
*/ */
public async executeSwapQuoteOrThrowAsync( public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteExecutionOpts>, opts: Partial<SwapQuoteExecutionOpts & ForwarderExtensionContractOpts>,
): Promise<string> { ): Promise<string> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync()); assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
const { ethAmount, takerAddress, gasLimit, gasPrice, feeRecipient, feePercentage } = _.merge( const { ethAmount: providedEthAmount, takerAddress, gasLimit, gasPrice, feeRecipient, feePercentage } = _.merge(
{}, {},
constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
opts, opts,
@ -169,8 +169,8 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
assert.isValidPercentage('feePercentage', feePercentage); assert.isValidPercentage('feePercentage', feePercentage);
assert.isETHAddressHex('feeRecipient', feeRecipient); assert.isETHAddressHex('feeRecipient', feeRecipient);
if (ethAmount !== undefined) { if (providedEthAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount); assert.isBigNumber('ethAmount', providedEthAmount);
} }
if (takerAddress !== undefined) { if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress); assert.isETHAddressHex('takerAddress', takerAddress);
@ -182,59 +182,50 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
assert.isBigNumber('gasPrice', gasPrice); assert.isBigNumber('gasPrice', gasPrice);
} }
const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, feePercentage); const { orders, worstCaseQuoteInfo } = quote; // tslint:disable-line:no-unused-variable
const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee; // tslint:disable-line:no-unused-variable
// get taker address // get taker address
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
// if no ethAmount is provided, default to the worst totalTakerTokenAmount // if no ethAmount is provided, default to the worst totalTakerAssetAmount
const value = ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount; const ethAmountWithFees = affiliateFeeUtils
.getTotalEthAmountWithAffiliateFee(worstCaseQuoteInfo, feePercentage)
.plus(worstCaseQuoteInfo.protocolFeeInEthAmount);
// format fee percentage // format fee percentage
const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage); const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage);
try { let txHash: string;
let txHash: string; if (quote.type === MarketOperation.Buy) {
if (quoteWithAffiliateFee.type === MarketOperation.Buy) { const { makerAssetFillAmount } = quote;
const { makerAssetFillAmount } = quoteWithAffiliateFee; txHash = await this._forwarder
txHash = await this._contractWrappers.forwarder .marketBuyOrdersWithEth(
.marketBuyOrdersWithEth( orders,
orders, makerAssetFillAmount,
makerAssetFillAmount, orders.map(o => o.signature),
orders.map(o => o.signature), formattedFeePercentage,
formattedFeePercentage, feeRecipient,
feeRecipient, )
) .sendTransactionAsync({
.sendTransactionAsync({ from: finalTakerAddress,
value, gas: gasLimit,
from: finalTakerAddress.toLowerCase(), gasPrice,
gas: gasLimit, value: providedEthAmount || ethAmountWithFees,
gasPrice, });
}); } else {
} else { txHash = await this._forwarder
txHash = await this._contractWrappers.forwarder .marketSellOrdersWithEth(orders, orders.map(o => o.signature), formattedFeePercentage, feeRecipient)
.marketSellOrdersWithEth(orders, orders.map(o => o.signature), formattedFeePercentage, feeRecipient) .sendTransactionAsync({
.sendTransactionAsync({ from: finalTakerAddress,
value, gas: gasLimit,
from: finalTakerAddress.toLowerCase(), gasPrice,
gas: gasLimit, value: providedEthAmount || ethAmountWithFees,
gasPrice, });
});
}
return txHash;
} catch (err) {
if (_.includes(err.message, ContractError.SignatureRequestDenied)) {
throw new Error(SwapQuoteConsumerError.SignatureRequestDenied);
} else if (_.includes(err.message, ForwarderError.CompleteFillFailed)) {
throw new Error(SwapQuoteConsumerError.TransactionValueTooLow);
} else {
throw err;
}
} }
// TODO(dorothy-zbornak): Handle signature request denied
// (see contract-wrappers/decorators)
// and ForwarderRevertErrors.CompleteBuyFailed.
return txHash;
} }
private async _getEtherTokenAssetDataOrThrowAsync(): Promise<string> { private async _getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._contractWrappers.devUtils return this._devUtils.encodeERC20AssetData(this._contractAddresses.etherToken).callAsync();
.encodeERC20AssetData(this._contractWrappers.contractAddresses.etherToken)
.callAsync();
} }
} }

View File

@ -1,4 +1,4 @@
import { ContractWrappers } from '@0x/contract-wrappers'; import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { providerUtils } from '@0x/utils'; import { providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -13,6 +13,7 @@ import {
SwapQuote, SwapQuote,
SwapQuoteConsumerBase, SwapQuoteConsumerBase,
SwapQuoteConsumerOpts, SwapQuoteConsumerOpts,
SwapQuoteConsumingOpts,
SwapQuoteExecutionOpts, SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts, SwapQuoteGetOutputOpts,
} from '../types'; } from '../types';
@ -28,7 +29,14 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer; private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer; private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
private readonly _contractWrappers: ContractWrappers; private readonly _contractAddresses: ContractAddresses;
public static getSwapQuoteConsumer(
supportedProvider: SupportedProvider,
options: Partial<SwapQuoteConsumerOpts> = {},
): SwapQuoteConsumer {
return new SwapQuoteConsumer(supportedProvider, options);
}
constructor(supportedProvider: SupportedProvider, options: Partial<SwapQuoteConsumerOpts> = {}) { constructor(supportedProvider: SupportedProvider, options: Partial<SwapQuoteConsumerOpts> = {}) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
@ -37,22 +45,19 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
const provider = providerUtils.standardizeOrThrow(supportedProvider); const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider; this.provider = provider;
this.chainId = chainId; this.chainId = chainId;
this._contractAddresses = getContractAddressesForChainOrThrow(chainId);
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, options); this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, options); this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._contractWrappers = new ContractWrappers(this.provider, {
chainId,
});
} }
/** /**
* Given a SwapQuote, returns 'CalldataInfo' for a 0x exchange call. See type definition of CalldataInfo for more information. * Given a SwapQuote, returns 'CalldataInfo' for a 0x extesion or exchange call. See type definition of CalldataInfo for more information.
* @param quote An object that conforms to SwapQuote. See type definition for more information. * @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting SmartContractParams. See type definition for more information. * @param opts Options for getting SmartContractParams. See type definition for more information.
*/ */
public async getCalldataOrThrowAsync( public async getCalldataOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {}, opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
): Promise<CalldataInfo> { ): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(opts); const consumer = await this._getConsumerForSwapQuoteAsync(opts);
@ -60,13 +65,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
} }
/** /**
* Given a SwapQuote, returns 'SmartContractParamsInfo' for a 0x exchange call. See type definition of SmartContractParamsInfo for more information. * Given a SwapQuote, returns 'SmartContractParamsInfo' for a 0x extension or exchange call. See type definition of SmartContractParamsInfo for more information.
* @param quote An object that conforms to SwapQuote. See type definition for more information. * @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting SmartContractParams. See type definition for more information. * @param opts Options for getting SmartContractParams. See type definition for more information.
*/ */
public async getSmartContractParamsOrThrowAsync( public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {}, opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
): Promise<SmartContractParamsInfo<SmartContractParams>> { ): Promise<SmartContractParamsInfo<SmartContractParams>> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(opts); const consumer = await this._getConsumerForSwapQuoteAsync(opts);
@ -74,33 +79,38 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
} }
/** /**
* Given a SwapQuote and desired rate (in takerAsset), attempt to execute the swap. * Given a SwapQuote and desired rate (in takerAsset), attempt to execute the swap with 0x extension or exchange contract.
* @param quote An object that conforms to SwapQuote. See type definition for more information. * @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting CalldataInfo. See type definition for more information. * @param opts Options for getting CalldataInfo. See type definition for more information.
*/ */
public async executeSwapQuoteOrThrowAsync( public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts> = {}, opts: Partial<SwapQuoteExecutionOpts & SwapQuoteConsumingOpts> = {},
): Promise<string> { ): Promise<string> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(opts); const consumer = await this._getConsumerForSwapQuoteAsync(opts);
return consumer.executeSwapQuoteOrThrowAsync(quote, opts); return consumer.executeSwapQuoteOrThrowAsync(quote, opts);
} }
/**
* Given a SwapQuote, returns optimal 0x protocol interface (extension or no extension) to perform the swap.
* @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting optimal exteion contract to fill quote. See type definition for more information.
*/
public async getOptimalExtensionContractTypeAsync( public async getOptimalExtensionContractTypeAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<GetExtensionContractTypeOpts> = {}, opts: Partial<GetExtensionContractTypeOpts> = {},
): Promise<ExtensionContractType> { ): Promise<ExtensionContractType> {
return swapQuoteConsumerUtils.getExtensionContractTypeForSwapQuoteAsync( return swapQuoteConsumerUtils.getExtensionContractTypeForSwapQuoteAsync(
quote, quote,
this._contractWrappers, this._contractAddresses,
this.provider, this.provider,
opts, opts,
); );
} }
private async _getConsumerForSwapQuoteAsync( private async _getConsumerForSwapQuoteAsync(
opts: Partial<SwapQuoteGetOutputOpts>, opts: Partial<SwapQuoteConsumingOpts>,
): Promise<SwapQuoteConsumerBase<SmartContractParams>> { ): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
if (opts.useExtensionContract === ExtensionContractType.Forwarder) { if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
return this._forwarderConsumer; return this._forwarderConsumer;

View File

@ -1,18 +1,20 @@
import { ContractWrappers } from '@0x/contract-wrappers'; import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { schemas } from '@0x/json-schemas'; import { schemas } from '@0x/json-schemas';
import { assetDataUtils, SignedOrder } from '@0x/order-utils'; import { SignedOrder } from '@0x/order-utils';
import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook'; import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { MarketOperation } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils'; import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types'; import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants } from './constants'; import { constants } from './constants';
import { import {
LiquidityForAssetData, LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote, MarketSellSwapQuote,
OrdersAndFillableAmounts, OrderPrunerPermittedFeeTypes,
PrunedSignedOrder,
SwapQuote, SwapQuote,
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
SwapQuoterError, SwapQuoterError,
@ -20,16 +22,19 @@ import {
} from './types'; } from './types';
import { assert } from './utils/assert'; import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity'; import { calculateLiquidity } from './utils/calculate_liquidity';
import { orderProviderResponseProcessor } from './utils/order_provider_response_processor'; import { OrderPruner } from './utils/order_prune_utils';
import { protocolFeeUtils } from './utils/protocol_fee_utils';
import { sortingUtils } from './utils/sorting_utils';
import { swapQuoteCalculator } from './utils/swap_quote_calculator'; import { swapQuoteCalculator } from './utils/swap_quote_calculator';
import { utils } from './utils/utils';
export class SwapQuoter { export class SwapQuoter {
public readonly provider: ZeroExProvider; public readonly provider: ZeroExProvider;
public readonly orderbook: Orderbook; public readonly orderbook: Orderbook;
public readonly expiryBufferMs: number; public readonly expiryBufferMs: number;
private readonly _contractWrappers: ContractWrappers; public readonly permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
private readonly _contractAddresses: ContractAddresses;
private readonly _orderPruner: OrderPruner;
private readonly _devUtilsContract: DevUtilsContract;
/** /**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network. * @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
@ -132,7 +137,11 @@ export class SwapQuoter {
* @return An instance of SwapQuoter * @return An instance of SwapQuoter
*/ */
constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: Partial<SwapQuoterOpts> = {}) { constructor(supportedProvider: SupportedProvider, orderbook: Orderbook, options: Partial<SwapQuoterOpts> = {}) {
const { chainId, expiryBufferMs } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); const { chainId, expiryBufferMs, permittedOrderFeeTypes } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTER_OPTS,
options,
);
const provider = providerUtils.standardizeOrThrow(supportedProvider); const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderbook('orderbook', orderbook); assert.isValidOrderbook('orderbook', orderbook);
assert.isNumber('chainId', chainId); assert.isNumber('chainId', chainId);
@ -140,10 +149,15 @@ export class SwapQuoter {
this.provider = provider; this.provider = provider;
this.orderbook = orderbook; this.orderbook = orderbook;
this.expiryBufferMs = expiryBufferMs; this.expiryBufferMs = expiryBufferMs;
this._contractWrappers = new ContractWrappers(this.provider, { this.permittedOrderFeeTypes = permittedOrderFeeTypes;
chainId, this._contractAddresses = getContractAddressesForChainOrThrow(chainId);
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._orderPruner = new OrderPruner(this._devUtilsContract, {
expiryBufferMs: this.expiryBufferMs,
permittedOrderFeeTypes: this.permittedOrderFeeTypes,
}); });
} }
/** /**
* Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address. * Get a `SwapQuote` containing all information relevant to fulfilling a swap between a desired ERC20 token address and ERC20 owned by a provided address.
* You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption. * You can then pass the `SwapQuote` to a `SwapQuoteConsumer` to execute a buy, or process SwapQuote for on-chain consumption.
@ -214,12 +228,8 @@ export class SwapQuoter {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount); assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount);
const makerAssetData = await this._contractWrappers.devUtils const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
.encodeERC20AssetData(makerTokenAddress) const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
.callAsync();
const takerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(takerTokenAddress)
.callAsync();
const swapQuote = this.getMarketBuySwapQuoteForAssetDataAsync( const swapQuote = this.getMarketBuySwapQuoteForAssetDataAsync(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
@ -248,12 +258,8 @@ export class SwapQuoter {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress); assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress); assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount); assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount);
const makerAssetData = await this._contractWrappers.devUtils const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
.encodeERC20AssetData(makerTokenAddress) const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
.callAsync();
const takerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(takerTokenAddress)
.callAsync();
const swapQuote = this.getMarketSellSwapQuoteForAssetDataAsync( const swapQuote = this.getMarketSellSwapQuoteForAssetDataAsync(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
@ -269,27 +275,26 @@ export class SwapQuoter {
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* *
* @return An object that conforms to LiquidityForAssetData that satisfies the request. See type definition for more information. * @return An object that conforms to LiquidityForTakerMakerAssetDataPair that satisfies the request. See type definition for more information.
*/ */
public async getLiquidityForMakerTakerAssetDataPairAsync( public async getLiquidityForMakerTakerAssetDataPairAsync(
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
): Promise<LiquidityForAssetData> { ): Promise<LiquidityForTakerMakerAssetDataPair> {
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData); const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) { if (!assetPairs.includes(makerAssetData)) {
return { return {
makerTokensAvailableInBaseUnits: new BigNumber(0), makerAssetAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0), takerAssetAvailableInBaseUnits: new BigNumber(0),
}; };
} }
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData); const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
return calculateLiquidity(ordersAndFillableAmounts); return calculateLiquidity(prunedOrders);
} }
/** /**
@ -299,7 +304,7 @@ export class SwapQuoter {
*/ */
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> { public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync(); const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs const assetPairs = allAssetPairs
.filter(pair => pair.assetDataA.assetData === makerAssetData) .filter(pair => pair.assetDataA.assetData === makerAssetData)
@ -314,7 +319,7 @@ export class SwapQuoter {
*/ */
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> { public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync(); const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs const assetPairs = allAssetPairs
.filter(pair => pair.assetDataB.assetData === takerAssetData) .filter(pair => pair.assetDataB.assetData === takerAssetData)
@ -324,6 +329,8 @@ export class SwapQuoter {
/** /**
* Validates the taker + maker asset pair is available from the order provider provided to `SwapQuote`. * Validates the taker + maker asset pair is available from the order provider provided to `SwapQuote`.
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* *
* @return A boolean on if the taker, maker pair exists * @return A boolean on if the taker, maker pair exists
*/ */
@ -333,41 +340,31 @@ export class SwapQuoter {
): Promise<boolean> { ): Promise<boolean> {
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData); const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
return _.includes(availableMakerAssetDatas, makerAssetData); return _.includes(availableMakerAssetDatas, makerAssetData);
} }
/** /**
* Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders * Grab orders from the order provider, prunes for valid orders with provided OrderPruner options
* @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param makerAssetData The makerAssetData of the desired asset to swap for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * @param takerAssetData The takerAssetData of the asset to swap makerAssetData for (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
*/ */
public async getOrdersAndFillableAmountsAsync( public async getPrunedSignedOrdersAsync(
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
): Promise<OrdersAndFillableAmounts> { ): Promise<PrunedSignedOrder[]> {
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
assetDataUtils.decodeAssetDataOrThrow(takerAssetData); await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
const zrxTokenAssetData = await this._getZrxTokenAssetDataOrThrowAsync();
// get orders // get orders
const response = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData); const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const adaptedResponse = { orders: response.map(o => ({ ...o.order, ...o.metaData })) }; const orders = _.map(apiOrders, o => o.order);
// since the order provider is an injected dependency, validate that it respects the API const prunedOrders = await this._orderPruner.pruneSignedOrdersAsync(orders);
// ie. it should only return maker/taker assetDatas that are specified const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders);
orderProviderResponseProcessor.throwIfInvalidResponse(adaptedResponse, makerAssetData, takerAssetData); return sortedPrunedOrders;
// process the responses into one object
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
adaptedResponse,
isMakerAssetZrxToken,
this.expiryBufferMs,
this._contractWrappers.orderValidator,
);
return ordersAndFillableAmounts;
} }
/** /**
@ -375,18 +372,16 @@ export class SwapQuoter {
* @param swapQuote The swapQuote in question to check enough allowance enabled for 0x exchange contracts to conduct the swap. * @param swapQuote The swapQuote in question to check enough allowance enabled for 0x exchange contracts to conduct the swap.
* @param takerAddress The address of the taker of the provided swapQuote * @param takerAddress The address of the taker of the provided swapQuote
*/ */
public async isTakerAddressAllowanceEnoughForBestAndWorstQuoteInfoAsync( public async isSwapQuoteFillableByTakerAddressAsync(
swapQuote: SwapQuote, swapQuote: SwapQuote,
takerAddress: string, takerAddress: string,
): Promise<[boolean, boolean]> { ): Promise<[boolean, boolean]> {
const orderValidator = this._contractWrappers.orderValidator; const balanceAndAllowance = await this._devUtilsContract
const balanceAndAllowance = await orderValidator .getBalanceAndAssetProxyAllowance(takerAddress, swapQuote.takerAssetData)
.getBalanceAndAllowance(takerAddress, swapQuote.takerAssetData)
.callAsync(); .callAsync();
const allowance = balanceAndAllowance[1];
return [ return [
allowance.isGreaterThanOrEqualTo(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount), balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.bestCaseQuoteInfo.totalTakerAssetAmount),
allowance.isGreaterThanOrEqualTo(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount), balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.worstCaseQuoteInfo.totalTakerAssetAmount),
]; ];
} }
@ -398,13 +393,10 @@ export class SwapQuoter {
} }
/** /**
* Get the assetData that represents the ZRX token. * Utility function to get assetData for Ether token.
* Will throw if ZRX does not exist for the current chain.
*/ */
private async _getZrxTokenAssetDataOrThrowAsync(): Promise<string> { public async getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._contractWrappers.devUtils return this._devUtilsContract.encodeERC20AssetData(this._contractAddresses.etherToken).callAsync();
.encodeERC20AssetData(this._contractWrappers.contractAddresses.zrxToken)
.callAsync();
} }
/** /**
@ -417,30 +409,20 @@ export class SwapQuoter {
marketOperation: MarketOperation, marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>, options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> { ): Promise<SwapQuote> {
const { slippagePercentage, shouldDisableRequestingFeeOrders } = _.merge( const { slippagePercentage } = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assert.isNumber('slippagePercentage', slippagePercentage); assert.isNumber('slippagePercentage', slippagePercentage);
const zrxTokenAssetData = await this._getZrxTokenAssetDataOrThrowAsync(); let gasPrice: BigNumber;
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData; if (!!options.gasPrice) {
// get the relevant orders for the makerAsset gasPrice = options.gasPrice;
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData); assert.isBigNumber('gasPrice', gasPrice);
const doesOrdersRequireFeeOrders = } else {
!isMakerAssetZrxToken && utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts); gasPrice = await protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
const isRequestingFeeOrders = !shouldDisableRequestingFeeOrders && doesOrdersRequireFeeOrders;
let feeOrdersAndFillableAmounts = constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS;
if (isRequestingFeeOrders) {
feeOrdersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
zrxTokenAssetData,
takerAssetData,
);
} }
// get the relevant orders for the makerAsset
if (ordersAndFillableAmounts.orders.length === 0) { const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
if (prunedOrders.length === 0) {
throw new Error( throw new Error(
`${ `${
SwapQuoterError.AssetUnavailable SwapQuoterError.AssetUnavailable
@ -448,33 +430,21 @@ export class SwapQuoter {
); );
} }
if (isRequestingFeeOrders && feeOrdersAndFillableAmounts.orders.length === 0) {
throw new Error(
`${
SwapQuoterError.FeeAssetUnavailable
}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`,
);
}
let swapQuote: SwapQuote; let swapQuote: SwapQuote;
if (marketOperation === MarketOperation.Buy) { if (marketOperation === MarketOperation.Buy) {
swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote( swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote(
ordersAndFillableAmounts, prunedOrders,
feeOrdersAndFillableAmounts,
assetFillAmount, assetFillAmount,
slippagePercentage, slippagePercentage,
isMakerAssetZrxToken, gasPrice,
shouldDisableRequestingFeeOrders,
); );
} else { } else {
swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote( swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts, prunedOrders,
feeOrdersAndFillableAmounts,
assetFillAmount, assetFillAmount,
slippagePercentage, slippagePercentage,
isMakerAssetZrxToken, gasPrice,
shouldDisableRequestingFeeOrders,
); );
} }

View File

@ -1,7 +1,27 @@
import { MarketOperation, SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { MethodAbi } from 'ethereum-types'; import { MethodAbi } from 'ethereum-types';
/**
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
* permittedOrderFeeTypes: A set of all the takerFee types that OrderPruner will filter for
*/
export interface OrderPrunerOpts {
expiryBufferMs: number;
permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
}
/**
* Represents the on-chain metadata of a signed order
*/
export interface OrderPrunerOnChainMetadata {
orderStatus: number;
orderHash: string;
orderTakerAssetFilledAmount: BigNumber;
fillableTakerAssetAmount: BigNumber;
isValidSignature: boolean;
}
/** /**
* makerAssetData: The assetData representing the desired makerAsset. * makerAssetData: The assetData representing the desired makerAsset.
* takerAssetData: The assetData representing the desired takerAsset. * takerAssetData: The assetData representing the desired takerAsset.
@ -12,18 +32,14 @@ export interface OrderProviderRequest {
} }
/** /**
* orders: An array of orders with optional remaining fillable makerAsset amounts. See type for more info. * fillableMakerAssetAmount: Amount of makerAsset that is fillable
* fillableTakerAssetAmount: Amount of takerAsset that is fillable
* fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAssetAmount
*/ */
export interface OrderProviderResponse { export interface PrunedSignedOrder extends SignedOrder {
orders: SignedOrderWithRemainingFillableMakerAssetAmount[]; fillableMakerAssetAmount: BigNumber;
} fillableTakerAssetAmount: BigNumber;
fillableTakerFeeAmount: BigNumber;
/**
* A normal SignedOrder with one extra optional property `remainingFillableMakerAssetAmount`
* remainingFillableMakerAssetAmount: The amount of the makerAsset that is available to be filled
*/
export interface SignedOrderWithRemainingFillableMakerAssetAmount extends SignedOrder {
remainingFillableMakerAssetAmount?: BigNumber;
} }
/** /**
@ -31,13 +47,13 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed
* calldataHexString: The hexstring of the calldata. * calldataHexString: The hexstring of the calldata.
* methodAbi: The ABI of the smart contract method to call. * methodAbi: The ABI of the smart contract method to call.
* toAddress: The contract address to call. * toAddress: The contract address to call.
* ethAmount: If provided, the eth amount in wei to send with the smart contract call. * ethAmount: The eth amount in wei to send with the smart contract call.
*/ */
export interface CalldataInfo { export interface CalldataInfo {
calldataHexString: string; calldataHexString: string;
methodAbi: MethodAbi; methodAbi: MethodAbi;
toAddress: string; toAddress: string;
ethAmount?: BigNumber; ethAmount: BigNumber;
} }
/** /**
@ -50,7 +66,7 @@ export interface CalldataInfo {
export interface SmartContractParamsInfo<T> { export interface SmartContractParamsInfo<T> {
params: T; params: T;
toAddress: string; toAddress: string;
ethAmount?: BigNumber; ethAmount: BigNumber;
methodAbi: MethodAbi; methodAbi: MethodAbi;
} }
@ -95,14 +111,10 @@ export enum ExtensionContractType {
export type ExchangeSmartContractParams = ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams; export type ExchangeSmartContractParams = ExchangeMarketBuySmartContractParams | ExchangeMarketSellSmartContractParams;
/** /**
* feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above.
* feeSignatures: An array of signatures that attest that the maker of the orders in fact made the orders.
* feePercentage: Optional affiliate fee percentage used to calculate the eth amount paid to fee recipient. * feePercentage: Optional affiliate fee percentage used to calculate the eth amount paid to fee recipient.
* feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
*/ */
export interface ForwarderSmartContractParamsBase { export interface ForwarderSmartContractParamsBase {
feeOrders: SignedOrder[];
feeSignatures: string[];
feePercentage: BigNumber; feePercentage: BigNumber;
feeRecipient: string; feeRecipient: string;
} }
@ -137,12 +149,12 @@ export type SmartContractParams = ForwarderSmartContractParams | ExchangeSmartCo
* executeSwapQuoteOrThrowAsync: Executes a web3 transaction to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided. * executeSwapQuoteOrThrowAsync: Executes a web3 transaction to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided.
*/ */
export interface SwapQuoteConsumerBase<T> { export interface SwapQuoteConsumerBase<T> {
getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteGetOutputOptsBase>): Promise<CalldataInfo>; getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteGetOutputOpts>): Promise<CalldataInfo>;
getSmartContractParamsOrThrowAsync( getSmartContractParamsOrThrowAsync(
quote: SwapQuote, quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOptsBase>, opts: Partial<SwapQuoteGetOutputOpts>,
): Promise<SmartContractParamsInfo<T>>; ): Promise<SmartContractParamsInfo<T>>;
executeSwapQuoteOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteExecutionOptsBase>): Promise<string>; executeSwapQuoteOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteExecutionOpts>): Promise<string>;
} }
/** /**
@ -155,28 +167,37 @@ export interface SwapQuoteConsumerOpts {
/** /**
* Represents the options provided to a generic SwapQuoteConsumer * Represents the options provided to a generic SwapQuoteConsumer
*/ */
export interface SwapQuoteGetOutputOptsBase {} export interface SwapQuoteGetOutputOpts {}
/** /**
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider. * takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
* gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call. * gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call.
* gasPrice: Gas price in Wei to use for a transaction * gasPrice: Gas price in Wei to use for a transaction
* ethAmount: The amount of eth sent with the execution of a swap
*/ */
export interface SwapQuoteExecutionOptsBase extends SwapQuoteGetOutputOptsBase { export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
takerAddress?: string; takerAddress?: string;
gasLimit?: number; gasLimit?: number;
gasPrice?: BigNumber; gasPrice?: BigNumber;
ethAmount?: BigNumber;
} }
/** /**
* ethAmount: The amount of eth (in Wei) sent to the forwarder contract.
* feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient * feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient
* feeRecipient: address of the receiver of the feePercentage of taker asset * feeRecipient: address of the receiver of the feePercentage of taker asset
* ethAmount: The amount of eth (in Wei) sent to the forwarder contract.
*/ */
export interface ForwarderSwapQuoteGetOutputOpts extends SwapQuoteGetOutputOptsBase { export interface ForwarderExtensionContractOpts {
ethAmount?: BigNumber;
feePercentage: number; feePercentage: number;
feeRecipient: string; feeRecipient: string;
ethAmount?: BigNumber; }
/*
* Options for how SwapQuoteConsumer will generate output
*/
export interface SwapQuoteConsumingOpts {
useExtensionContract: ExtensionContractType;
} }
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
@ -186,26 +207,10 @@ export interface GetExtensionContractTypeOpts {
ethAmount?: BigNumber; ethAmount?: BigNumber;
} }
/**
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
* useConsumerType: If provided, defaults the SwapQuoteConsumer to create output consumed by ConsumerType.
*/
export interface SwapQuoteGetOutputOpts extends ForwarderSwapQuoteGetOutputOpts {
useExtensionContract: ExtensionContractType;
}
export interface ForwarderSwapQuoteExecutionOpts extends ForwarderSwapQuoteGetOutputOpts, SwapQuoteExecutionOptsBase {}
/**
* Represents the options for executing a swap quote with SwapQuoteConsumer
*/
export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts, ForwarderSwapQuoteExecutionOpts {}
/** /**
* takerAssetData: String that represents a specific taker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * takerAssetData: String that represents a specific taker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* makerAssetData: String that represents a specific maker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). * makerAssetData: String that represents a specific maker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage. * orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage.
* feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above.
* bestCaseQuoteInfo: Info about the best case price for the asset. * bestCaseQuoteInfo: Info about the best case price for the asset.
* worstCaseQuoteInfo: Info about the worst case price for the asset. * worstCaseQuoteInfo: Info about the worst case price for the asset.
*/ */
@ -213,7 +218,6 @@ export interface SwapQuoteBase {
takerAssetData: string; takerAssetData: string;
makerAssetData: string; makerAssetData: string;
orders: SignedOrder[]; orders: SignedOrder[];
feeOrders: SignedOrder[];
bestCaseQuoteInfo: SwapQuoteInfo; bestCaseQuoteInfo: SwapQuoteInfo;
worstCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo;
} }
@ -236,36 +240,28 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
type: MarketOperation.Buy; type: MarketOperation.Buy;
} }
export interface SwapQuoteWithAffiliateFeeBase {
feePercentage: number;
}
export interface MarketSellSwapQuoteWithAffiliateFee extends SwapQuoteWithAffiliateFeeBase, MarketSellSwapQuote {}
export interface MarketBuySwapQuoteWithAffiliateFee extends SwapQuoteWithAffiliateFeeBase, MarketBuySwapQuote {}
export type SwapQuoteWithAffiliateFee = MarketBuySwapQuoteWithAffiliateFee | MarketSellSwapQuoteWithAffiliateFee;
/** /**
* feeTakerTokenAmount: The amount of takerToken required any fee concerned with completing the swap. * feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerTokenAmount: The amount of takerToken required to conduct the swap. * takerAssetAmount: The amount of takerAsset swapped for desired makerAsset.
* totalTakerTokenAmount: The total amount of takerToken required to complete the swap (filling orders, feeOrders, and paying affiliate fee) * totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* makerTokenAmount: The amount of makerToken that will be acquired through the swap. * makerAssetAmount: The amount of makerAsset that will be acquired through the swap.
* protocolFeeInEthAmount: The amount of eth to pay as protocol fee to perform the swap for desired asset.
*/ */
export interface SwapQuoteInfo { export interface SwapQuoteInfo {
feeTakerTokenAmount: BigNumber; feeTakerAssetAmount: BigNumber;
totalTakerTokenAmount: BigNumber; takerAssetAmount: BigNumber;
takerTokenAmount: BigNumber; totalTakerAssetAmount: BigNumber;
makerTokenAmount: BigNumber; makerAssetAmount: BigNumber;
protocolFeeInEthAmount: BigNumber;
} }
/** /**
* shouldDisableRequestingFeeOrders: If set to true, requesting a swapQuote will not perform any computation or requests for fees.
* slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%). * slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
* gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount
*/ */
export interface SwapQuoteRequestOpts { export interface SwapQuoteRequestOpts {
shouldDisableRequestingFeeOrders: boolean;
slippagePercentage: number; slippagePercentage: number;
gasPrice?: BigNumber;
} }
/** /**
@ -273,7 +269,7 @@ export interface SwapQuoteRequestOpts {
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). * orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
*/ */
export interface SwapQuoterOpts { export interface SwapQuoterOpts extends OrderPrunerOpts {
chainId: number; chainId: number;
orderRefreshIntervalMs: number; orderRefreshIntervalMs: number;
expiryBufferMs: number; expiryBufferMs: number;
@ -295,28 +291,33 @@ export enum SwapQuoteConsumerError {
*/ */
export enum SwapQuoterError { export enum SwapQuoterError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND', NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR', StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR',
InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY', InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY',
InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY',
InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE',
AssetUnavailable = 'ASSET_UNAVAILABLE', AssetUnavailable = 'ASSET_UNAVAILABLE',
FeeAssetUnavailable = 'FEE_ASSET_UNAVAILABLE', NoGasPriceProvidedOrEstimated = 'NO_GAS_PRICE_PROVIDED_OR_ESTIMATED',
} }
/** /**
* orders: An array of signed orders * Represents available liquidity for a given assetData.
* remainingFillableMakerAssetAmounts: A list of fillable amounts for the signed orders. The index of an item in the array associates the amount with the corresponding order.
*/ */
export interface OrdersAndFillableAmounts { export interface LiquidityForTakerMakerAssetDataPair {
orders: SignedOrder[]; makerAssetAvailableInBaseUnits: BigNumber;
remainingFillableMakerAssetAmounts: BigNumber[]; takerAssetAvailableInBaseUnits: BigNumber;
} }
/** /**
* Represents available liquidity for a given assetData * Represents two main market operations supported by asset-swapper.
*/ */
export interface LiquidityForAssetData { export enum MarketOperation {
makerTokensAvailableInBaseUnits: BigNumber; Sell = 'Sell',
takerTokensAvailableInBaseUnits: BigNumber; Buy = 'Buy',
}
/**
* Represents varying order takerFee types that can be pruned for by OrderPruner.
*/
export enum OrderPrunerPermittedFeeTypes {
NoFees = 'NO_FEES',
MakerDenominatedTakerFee = 'MAKER_DENOMINATED_TAKER_FEE',
TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE',
} }

View File

@ -1,29 +1,24 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { SwapQuote, SwapQuoteInfo, SwapQuoteWithAffiliateFee } from '../types'; import { constants } from '../constants';
import { SwapQuoteInfo } from '../types';
import { assert } from './assert';
export const affiliateFeeUtils = { export const affiliateFeeUtils = {
getSwapQuoteWithAffiliateFee(quote: SwapQuote, feePercentage: number): SwapQuoteWithAffiliateFee { /**
const newQuote = _.clone(quote); * Get the amount of eth to send for a forwarder contract call (includes takerAssetAmount, protocol fees, and specified affiliate fee amount)
newQuote.bestCaseQuoteInfo = getSwapQuoteInfoWithAffiliateFee(newQuote.bestCaseQuoteInfo, feePercentage); * @param swapQuoteInfo SwapQuoteInfo to generate total eth amount from
newQuote.worstCaseQuoteInfo = getSwapQuoteInfoWithAffiliateFee(newQuote.worstCaseQuoteInfo, feePercentage); * @param feePercentage Percentage of additive fees to apply to totalTakerAssetAmount + protocol fee. (max 5%)
return { ...newQuote, ...{ feePercentage } }; */
getTotalEthAmountWithAffiliateFee(swapQuoteInfo: SwapQuoteInfo, feePercentage: number): BigNumber {
assert.assert(
feePercentage >= 0 && feePercentage <= constants.MAX_AFFILIATE_FEE_PERCENTAGE,
'feePercentage must be between range 0-0.05 (inclusive)',
);
const ethAmount = swapQuoteInfo.protocolFeeInEthAmount.plus(swapQuoteInfo.totalTakerAssetAmount);
const affiliateFeeAmount = ethAmount.multipliedBy(feePercentage);
const ethAmountWithFees = ethAmount.plus(affiliateFeeAmount);
return ethAmountWithFees;
}, },
}; };
/**
* Adds a fee based on feePercentage of the takerTokenAmount and adds it to the feeTakerTokenAmount and totalTakerTokenAmount
* @param quoteInfo quote information to add fee to
* @param feePercentage the percentage of takerTokenAmount charged additionally as a fee
*/
const getSwapQuoteInfoWithAffiliateFee = (quoteInfo: SwapQuoteInfo, feePercentage: number): SwapQuoteInfo => {
const newQuoteInfo = _.clone(quoteInfo);
const affiliateFeeAmount = quoteInfo.takerTokenAmount
.multipliedBy(feePercentage)
.integerValue(BigNumber.ROUND_CEIL);
const newFeeAmount = quoteInfo.feeTakerTokenAmount.plus(affiliateFeeAmount);
newQuoteInfo.feeTakerTokenAmount = newFeeAmount;
newQuoteInfo.totalTakerTokenAmount = newFeeAmount.plus(quoteInfo.takerTokenAmount);
return newQuoteInfo;
};

View File

@ -1,10 +1,12 @@
import { assert as sharedAssert } from '@0x/assert'; import { assert as sharedAssert } from '@0x/assert';
import { schemas } from '@0x/json-schemas'; import { schemas } from '@0x/json-schemas';
import { Orderbook } from '@0x/orderbook'; import { Orderbook } from '@0x/orderbook';
import { MarketOperation, SignedOrder } from '@0x/types'; import { Order, SignedOrder } from '@0x/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types'; import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { utils } from './utils';
export const assert = { export const assert = {
...sharedAssert, ...sharedAssert,
@ -12,8 +14,7 @@ export const assert = {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData); sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData); sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema); sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema); assert.isValidSwapQuoteOrders(
assert.isValidOrdersForSwapQuote(
`${variableName}.orders`, `${variableName}.orders`,
swapQuote.orders, swapQuote.orders,
swapQuote.makerAssetData, swapQuote.makerAssetData,
@ -27,7 +28,7 @@ export const assert = {
sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount); sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount);
} }
}, },
isValidOrdersForSwapQuote( isValidSwapQuoteOrders(
variableName: string, variableName: string,
orders: SignedOrder[], orders: SignedOrder[],
makerAssetData: string, makerAssetData: string,
@ -48,10 +49,21 @@ export const assert = {
); );
}); });
}, },
isValidOrdersForSwapQuoter<T extends Order>(variableName: string, orders: T[]): void {
_.every(orders, (order: T, index: number) => {
assert.assert(
order.takerFee.isZero() ||
utils.isOrderTakerFeePayableWithTakerAsset(order) ||
utils.isOrderTakerFeePayableWithMakerAsset(order),
`Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${
order.takerAssetData
} but found ${order.takerFeeAssetData}`,
);
});
},
isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void { isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void {
assert.isValidSwapQuote(variableName, swapQuote); assert.isValidSwapQuote(variableName, swapQuote);
assert.isValidForwarderSignedOrders(`${variableName}.orders`, swapQuote.orders, wethAssetData); assert.isValidForwarderSignedOrders(`${variableName}.orders`, swapQuote.orders, wethAssetData);
assert.isValidForwarderSignedOrders(`${variableName}.feeOrders`, swapQuote.feeOrders, wethAssetData);
}, },
isValidForwarderSignedOrders(variableName: string, orders: SignedOrder[], wethAssetData: string): void { isValidForwarderSignedOrders(variableName: string, orders: SignedOrder[], wethAssetData: string): void {
_.forEach(orders, (o: SignedOrder, i: number) => { _.forEach(orders, (o: SignedOrder, i: number) => {
@ -65,10 +77,10 @@ export const assert = {
); );
}, },
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void { isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, swapQuoteInfo.feeTakerTokenAmount); sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, swapQuoteInfo.totalTakerTokenAmount); sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount); sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.makerTokenAmount); sharedAssert.isBigNumber(`${variableName}.makerAssetAmount`, swapQuoteInfo.makerAssetAmount);
}, },
isValidOrderbook(variableName: string, orderFetcher: Orderbook): void { isValidOrderbook(variableName: string, orderFetcher: Orderbook): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync); sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);

View File

@ -1,40 +1,27 @@
import { orderCalculationUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../types'; import { LiquidityForTakerMakerAssetDataPair, PrunedSignedOrder } from '../types';
export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAmounts): LiquidityForAssetData => { import { utils } from './utils';
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const liquidityInBigNumbers = orders.reduce(
(acc, order, curIndex) => {
const availableMakerAssetAmount = remainingFillableMakerAssetAmounts[curIndex];
if (availableMakerAssetAmount === undefined) {
throw new Error(`No corresponding fillableMakerAssetAmounts at index ${curIndex}`);
}
const makerTokensAvailableForCurrentOrder = availableMakerAssetAmount; export const calculateLiquidity = (prunedOrders: PrunedSignedOrder[]): LiquidityForTakerMakerAssetDataPair => {
const takerTokensAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount( const liquidityInBigNumbers = prunedOrders.reduce(
order, (acc, order) => {
makerTokensAvailableForCurrentOrder, const fillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
); ? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
: order.fillableMakerAssetAmount;
const fillableTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
: order.fillableTakerAssetAmount;
return { return {
makerTokensAvailableInBaseUnits: acc.makerTokensAvailableInBaseUnits.plus( makerAssetAvailableInBaseUnits: acc.makerAssetAvailableInBaseUnits.plus(fillableMakerAssetAmount),
makerTokensAvailableForCurrentOrder, takerAssetAvailableInBaseUnits: acc.takerAssetAvailableInBaseUnits.plus(fillableTakerAssetAmount),
),
takerTokensAvailableInBaseUnits: acc.takerTokensAvailableInBaseUnits.plus(
takerTokensAvailableForCurrentOrder,
),
}; };
}, },
{ {
makerTokensAvailableInBaseUnits: new BigNumber(0), makerAssetAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0), takerAssetAvailableInBaseUnits: new BigNumber(0),
}, },
); );
return liquidityInBigNumbers;
// Turn into regular numbers
return {
makerTokensAvailableInBaseUnits: liquidityInBigNumbers.makerTokensAvailableInBaseUnits,
takerTokensAvailableInBaseUnits: liquidityInBigNumbers.takerTokensAvailableInBaseUnits,
};
}; };

View File

@ -0,0 +1,92 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import { MarketOperation, PrunedSignedOrder } from '../types';
import { assert } from './assert';
import { utils } from './utils';
export const marketUtils = {
findOrdersThatCoverTakerAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
takerAssetFillAmount: BigNumber,
slippageBufferAmount: BigNumber = new BigNumber(0),
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
return findOrdersThatCoverAssetFillAmount(
sortedOrders,
takerAssetFillAmount,
MarketOperation.Sell,
slippageBufferAmount,
);
},
findOrdersThatCoverMakerAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
makerAssetFillAmount: BigNumber,
slippageBufferAmount: BigNumber = new BigNumber(0),
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
return findOrdersThatCoverAssetFillAmount(
sortedOrders,
makerAssetFillAmount,
MarketOperation.Buy,
slippageBufferAmount,
);
},
};
function findOrdersThatCoverAssetFillAmount(
sortedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
operation: MarketOperation,
slippageBufferAmount: BigNumber,
): { resultOrders: PrunedSignedOrder[]; remainingFillAmount: BigNumber } {
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
// calculate total amount of asset needed to be filled
const totalFillAmount = assetFillAmount.plus(slippageBufferAmount);
// iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount
const result = _.reduce(
sortedOrders,
({ resultOrders, remainingFillAmount }, order) => {
if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) {
return {
resultOrders,
remainingFillAmount: constants.ZERO_AMOUNT,
};
} else {
const assetAmountAvailable = getAssetAmountAvailable(order, operation);
const shouldIncludeOrder = assetAmountAvailable.gt(constants.ZERO_AMOUNT);
// if there is no assetAmountAvailable do not append order to resultOrders
// if we have exceeded the total amount we want to fill set remainingFillAmount to 0
return {
resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders,
remainingFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingFillAmount.minus(assetAmountAvailable),
),
};
}
},
{
resultOrders: [] as PrunedSignedOrder[],
remainingFillAmount: totalFillAmount,
},
);
return result;
}
function getAssetAmountAvailable(order: PrunedSignedOrder, operation: MarketOperation): BigNumber {
if (operation === MarketOperation.Buy) {
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else {
return order.fillableMakerAssetAmount;
}
} else {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else {
return order.fillableTakerAssetAmount;
}
}
}

View File

@ -1,182 +0,0 @@
import { OrderStatus, OrderValidatorContract } from '@0x/contract-wrappers';
import { orderCalculationUtils, sortingUtils } from '@0x/order-utils';
import { RemainingFillableCalculator } from '@0x/order-utils/lib/src/remaining_fillable_calculator';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
OrderProviderResponse,
OrdersAndFillableAmounts,
SignedOrderWithRemainingFillableMakerAssetAmount,
SwapQuoterError,
} from '../types';
export const orderProviderResponseProcessor = {
throwIfInvalidResponse(response: OrderProviderResponse, makerAssetData: string, takerAssetData: string): void {
_.forEach(response.orders, order => {
if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) {
throw new Error(SwapQuoterError.InvalidOrderProviderResponse);
}
});
},
/**
* Take the responses for the target orders to buy and fee orders and process them.
* Processing includes:
* - Drop orders that are expired or not open orders (null taker address)
* - If an orderValidator is provided, attempt to grab fillable amounts from on-chain otherwise assume completely fillable
* - Sort by rate
*/
async processAsync(
orderProviderResponse: OrderProviderResponse,
isMakerAssetZrxToken: boolean,
expiryBufferMs: number,
orderValidator?: OrderValidatorContract,
): Promise<OrdersAndFillableAmounts> {
// drop orders that are expired or not open
const filteredOrders = filterOutExpiredAndNonOpenOrders(
orderProviderResponse.orders,
expiryBufferMs / constants.ONE_SECOND_MS,
);
// set the orders to be sorted equal to the filtered orders
let unsortedOrders = filteredOrders;
// if an orderValidator is provided, use on chain information to calculate remaining fillable makerAsset amounts
if (orderValidator !== undefined) {
const takerAddresses = _.map(filteredOrders, () => constants.NULL_ADDRESS);
try {
const [ordersInfo, tradersInfo] = await orderValidator
.getOrdersAndTradersInfo(filteredOrders, takerAddresses)
.callAsync();
const ordersAndTradersInfo: any[] = ordersInfo.map((orderInfo, index) => {
const singleOrderAndTraderInfo = {
orderInfo,
traderInfo: tradersInfo[index],
};
return singleOrderAndTraderInfo;
});
// take orders + on chain information and find the valid orders and remaining fillable maker asset amounts
unsortedOrders = getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
filteredOrders,
ordersAndTradersInfo,
isMakerAssetZrxToken,
);
} catch (err) {
// Sometimes we observe this call to orderValidator fail with response `0x`
// Because of differences in Parity / Geth implementations, its very hard to tell if this response is a "system error"
// or a revert. In this case we just swallow these errors and fallback to partial fill information from the SRA.
// TODO(bmillman): report these errors so we have an idea of how often we're getting these failures.
}
}
// sort orders by rate
// TODO(bmillman): optimization
// provide a feeRate to the sorting function to more accurately sort based on the current market for ZRX tokens
const sortedOrders = isMakerAssetZrxToken
? sortingUtils.sortFeeOrdersByFeeAdjustedRate(unsortedOrders)
: sortingUtils.sortOrdersByFeeAdjustedRate(unsortedOrders);
// unbundle orders and fillable amounts and compile final result
const result = unbundleOrdersWithAmounts(sortedOrders);
return result;
},
};
/**
* Given an array of orders, return a new array with expired and non open orders filtered out.
*/
function filterOutExpiredAndNonOpenOrders(
orders: SignedOrderWithRemainingFillableMakerAssetAmount[],
expiryBufferMs: number,
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
const result = _.filter(orders, order => {
return (
orderCalculationUtils.isOpenOrder(order) &&
!orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS)
);
});
return result;
}
/**
* Given an array of orders and corresponding on-chain infos, return a subset of the orders
* that are still fillable orders with their corresponding remainingFillableMakerAssetAmounts.
*/
function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
inputOrders: SignedOrder[],
ordersAndTradersInfo: any[],
isMakerAssetZrxToken: boolean,
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
// iterate through the input orders and find the ones that are still fillable
// for the orders that are still fillable, calculate the remaining fillable maker asset amount
const result = _.reduce(
inputOrders,
(accOrders, order, index) => {
// get corresponding on-chain state for the order
const { orderInfo, traderInfo } = ordersAndTradersInfo[index];
// if the order IS NOT fillable, do not add anything to the accumulations and continue iterating
if (orderInfo.orderStatus !== OrderStatus.Fillable) {
return accOrders;
}
// if the order IS fillable, add the order and calculate the remaining fillable amount
const transferrableAssetAmount = BigNumber.min(traderInfo.makerAllowance, traderInfo.makerBalance);
const transferrableFeeAssetAmount = BigNumber.min(traderInfo.makerZrxAllowance, traderInfo.makerZrxBalance);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
const remainingMakerAssetAmount = orderCalculationUtils.getMakerFillAmount(
order,
remainingTakerAssetAmount,
);
const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee,
order.makerAssetAmount,
isMakerAssetZrxToken,
transferrableAssetAmount,
transferrableFeeAssetAmount,
remainingMakerAssetAmount,
);
const remainingFillableAmount = remainingFillableCalculator.computeRemainingFillable();
// if the order does not have any remaining fillable makerAsset, do not add anything to the accumulations and continue iterating
if (remainingFillableAmount.lte(constants.ZERO_AMOUNT)) {
return accOrders;
}
const orderWithRemainingFillableMakerAssetAmount = {
...order,
remainingFillableMakerAssetAmount: remainingFillableAmount,
};
const newAccOrders = _.concat(accOrders, orderWithRemainingFillableMakerAssetAmount);
return newAccOrders;
},
[] as SignedOrderWithRemainingFillableMakerAssetAmount[],
);
return result;
}
/**
* Given an array of orders with remaining fillable maker asset amounts. Unbundle into an instance of OrdersAndRemainingFillableMakerAssetAmounts.
* If an order is missing a corresponding remainingFillableMakerAssetAmount, assume it is completely fillable.
*/
function unbundleOrdersWithAmounts(
ordersWithAmounts: SignedOrderWithRemainingFillableMakerAssetAmount[],
): OrdersAndFillableAmounts {
const result = _.reduce(
ordersWithAmounts,
(acc, orderWithAmount) => {
const { orders, remainingFillableMakerAssetAmounts } = acc;
const { remainingFillableMakerAssetAmount, ...order } = orderWithAmount;
// if we are still missing a remainingFillableMakerAssetAmount, assume the order is completely fillable
const newRemainingAmount = remainingFillableMakerAssetAmount || order.makerAssetAmount;
// if remaining amount is less than or equal to zero, do not add it
if (newRemainingAmount.lte(constants.ZERO_AMOUNT)) {
return acc;
}
const newAcc = {
orders: _.concat(orders, order),
remainingFillableMakerAssetAmounts: _.concat(remainingFillableMakerAssetAmounts, newRemainingAmount),
};
return newAcc;
},
{
orders: [] as SignedOrder[],
remainingFillableMakerAssetAmounts: [] as BigNumber[],
},
);
return result;
}

View File

@ -0,0 +1,96 @@
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { orderCalculationUtils } from '@0x/order-utils';
import { OrderStatus, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { OrderPrunerOnChainMetadata, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, PrunedSignedOrder } from '../types';
import { utils } from '../utils/utils';
export class OrderPruner {
public readonly expiryBufferMs: number;
public readonly permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
private readonly _devUtils: DevUtilsContract;
// TODO(dave4506): OrderPruneCalculator can be more powerful if it takes in a specified takerAddress
constructor(devUtils: DevUtilsContract, opts: Partial<OrderPrunerOpts> = {}) {
const { expiryBufferMs, permittedOrderFeeTypes } = _.assign({}, constants.DEFAULT_ORDER_PRUNER_OPTS, opts);
this.expiryBufferMs = expiryBufferMs;
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._devUtils = devUtils;
}
public async pruneSignedOrdersAsync(signedOrders: SignedOrder[]): Promise<PrunedSignedOrder[]> {
const unsortedOrders = this._filterForUsableOrders(signedOrders, this.expiryBufferMs);
const signatures = _.map(unsortedOrders, o => o.signature);
const [ordersInfo, fillableTakerAssetAmounts, isValidSignatures] = await this._devUtils
.getOrderRelevantStates(unsortedOrders, signatures)
.callAsync();
const ordersOnChainMetadata: OrderPrunerOnChainMetadata[] = ordersInfo.map((orderInfo, index) => {
return {
...orderInfo,
fillableTakerAssetAmount: fillableTakerAssetAmounts[index],
isValidSignature: isValidSignatures[index],
};
});
// take orders + on chain information and find the valid orders and fillable makerAsset or takerAsset amounts
const prunedOrders = this._filterForFillableAndPermittedFeeTypeOrders(unsortedOrders, ordersOnChainMetadata);
return prunedOrders;
}
// tslint:disable-next-line: prefer-function-over-method
private _filterForFillableAndPermittedFeeTypeOrders(
orders: SignedOrder[],
ordersOnChainMetadata: OrderPrunerOnChainMetadata[],
): PrunedSignedOrder[] {
const result = _.chain(orders)
.filter(
(order: SignedOrder, index: number): boolean => {
const { isValidSignature, orderStatus } = ordersOnChainMetadata[index];
return (
isValidSignature &&
orderStatus === OrderStatus.Fillable &&
((this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithTakerAsset(order)) ||
(this.permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithMakerAsset(order)))
);
},
)
.map(
(order: SignedOrder, index: number): PrunedSignedOrder => {
const { fillableTakerAssetAmount } = ordersOnChainMetadata[index];
return {
...order,
fillableTakerAssetAmount,
fillableMakerAssetAmount: orderCalculationUtils.getMakerFillAmount(
order,
fillableTakerAssetAmount,
),
fillableTakerFeeAmount: orderCalculationUtils.getTakerFeeAmount(
order,
fillableTakerAssetAmount,
),
};
},
)
.value();
return result;
}
// tslint:disable-next-line: prefer-function-over-method
private _filterForUsableOrders(orders: SignedOrder[], expiryBufferMs: number): SignedOrder[] {
const result = _.filter(orders, order => {
return (
orderCalculationUtils.isOpenOrder(order) &&
!orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS)
);
});
return result;
}
}

View File

@ -0,0 +1,31 @@
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import { SwapQuoterError } from '../types';
// tslint:disable:no-unnecessary-type-assertion
export const protocolFeeUtils = {
/**
* Gets 'fast' gas price from Eth Gas Station.
*/
async getGasPriceEstimationOrThrowAsync(): Promise<BigNumber> {
try {
const res = await fetch(`${constants.ETH_GAS_STATION_API_BASE_URL}/json/ethgasAPI.json`);
const gasInfo = await res.json();
// Eth Gas Station result is gwei * 10
// tslint:disable-next-line:custom-no-magic-numbers
return new BigNumber(gasInfo.fast / 10);
} catch (e) {
throw new Error(SwapQuoterError.NoGasPriceProvidedOrEstimated);
}
},
/**
* Calculates protocol fee with protofol fee multiplier for each fill.
*/
calculateWorstCaseProtocolFee<T extends Order>(orders: T[], gasPrice: BigNumber): BigNumber {
const protocolFee = new BigNumber(orders.length * constants.PROTOCOL_FEE_MULTIPLIER).times(gasPrice);
return protocolFee;
},
};

View File

@ -0,0 +1,29 @@
import { schemas } from '@0x/json-schemas';
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { utils } from './utils';
export const sortingUtils = {
sortOrders<T extends Order>(orders: T[]): T[] {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.isValidOrdersForSwapQuoter('orders', orders);
const copiedOrders = _.cloneDeep(orders);
copiedOrders.sort((firstOrder, secondOrder) => {
const firstOrderRate = getTakerFeeAdjustedRateOfOrder(firstOrder);
const secondOrderRate = getTakerFeeAdjustedRateOfOrder(secondOrder);
return firstOrderRate.comparedTo(secondOrderRate);
});
return copiedOrders;
},
};
function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber {
const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = utils.getAdjustedMakerAndTakerAmountsFromTakerFees(
order,
);
const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount);
return rate;
}

View File

@ -1,5 +1,4 @@
import { marketUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; import { orderCalculationUtils } from '@0x/order-utils';
import { MarketOperation } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -7,106 +6,75 @@ import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors'; import { InsufficientAssetLiquidityError } from '../errors';
import { import {
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote, MarketSellSwapQuote,
OrdersAndFillableAmounts, PrunedSignedOrder,
SwapQuote, SwapQuote,
SwapQuoteInfo, SwapQuoteInfo,
SwapQuoterError,
} from '../types'; } from '../types';
import { marketUtils } from './market_utils';
import { protocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils';
// Calculates a swap quote for orders // Calculates a swap quote for orders
export const swapQuoteCalculator = { export const swapQuoteCalculator = {
calculateMarketSellSwapQuote( calculateMarketSellSwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
takerAssetFillAmount: BigNumber, takerAssetFillAmount: BigNumber,
slippagePercentage: number, slippagePercentage: number,
isMakerAssetZrxToken: boolean, gasPrice: BigNumber,
shouldDisableFeeOrderCalculations: boolean,
): MarketSellSwapQuote { ): MarketSellSwapQuote {
return calculateSwapQuote( return calculateSwapQuote(
ordersAndFillableAmounts, prunedOrders,
feeOrdersAndFillableAmounts,
takerAssetFillAmount, takerAssetFillAmount,
slippagePercentage, slippagePercentage,
isMakerAssetZrxToken, gasPrice,
shouldDisableFeeOrderCalculations,
MarketOperation.Sell, MarketOperation.Sell,
) as MarketSellSwapQuote; ) as MarketSellSwapQuote;
}, },
calculateMarketBuySwapQuote( calculateMarketBuySwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, takerAssetFillAmount: BigNumber,
makerAssetFillAmount: BigNumber,
slippagePercentage: number, slippagePercentage: number,
isMakerAssetZrxToken: boolean, gasPrice: BigNumber,
shouldDisableFeeOrderCalculations: boolean,
): MarketBuySwapQuote { ): MarketBuySwapQuote {
return calculateSwapQuote( return calculateSwapQuote(
ordersAndFillableAmounts, prunedOrders,
feeOrdersAndFillableAmounts, takerAssetFillAmount,
makerAssetFillAmount,
slippagePercentage, slippagePercentage,
isMakerAssetZrxToken, gasPrice,
shouldDisableFeeOrderCalculations,
MarketOperation.Buy, MarketOperation.Buy,
) as MarketBuySwapQuote; ) as MarketBuySwapQuote;
}, },
}; };
function calculateSwapQuote( function calculateSwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
slippagePercentage: number, slippagePercentage: number,
isMakerAssetZrxToken: boolean, gasPrice: BigNumber,
shouldDisableFeeOrderCalculations: boolean,
marketOperation: MarketOperation, marketOperation: MarketOperation,
): SwapQuote { ): SwapQuote {
const orders = ordersAndFillableAmounts.orders;
const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const remainingFillableTakerAssetAmounts = remainingFillableMakerAssetAmounts.map(
(makerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getTakerFillAmount(orders[index], makerAssetAmount);
},
);
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue(); const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: SignedOrder[]; let resultOrders: PrunedSignedOrder[];
let remainingFillAmount: BigNumber; let remainingFillAmount: BigNumber;
let ordersRemainingFillableMakerAssetAmounts: BigNumber[];
if (marketOperation === MarketOperation.Buy) { if (marketOperation === MarketOperation.Buy) {
// find the orders that cover the desired assetBuyAmount (with slippage) // find the orders that cover the desired assetBuyAmount (with slippage)
({ ({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
resultOrders, prunedOrders,
remainingFillAmount, assetFillAmount,
ordersRemainingFillableMakerAssetAmounts,
} = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetFillAmount, {
remainingFillableMakerAssetAmounts,
slippageBufferAmount, slippageBufferAmount,
})); ));
} else { } else {
let ordersRemainingFillableTakerAssetAmounts: BigNumber[];
// find the orders that cover the desired assetBuyAmount (with slippage) // find the orders that cover the desired assetBuyAmount (with slippage)
({ ({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
resultOrders, prunedOrders,
remainingFillAmount, assetFillAmount,
ordersRemainingFillableTakerAssetAmounts,
} = marketUtils.findOrdersThatCoverTakerAssetFillAmount(orders, assetFillAmount, {
remainingFillableTakerAssetAmounts,
slippageBufferAmount, slippageBufferAmount,
})); ));
ordersRemainingFillableMakerAssetAmounts = _.map(
ordersRemainingFillableTakerAssetAmounts,
(takerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getMakerFillAmount(resultOrders[index], takerAssetAmount);
},
);
} }
// if we do not have enough orders to cover the desired assetBuyAmount, throw // if we do not have enough orders to cover the desired assetBuyAmount, throw
@ -126,60 +94,16 @@ function calculateSwapQuote(
throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage); throw new InsufficientAssetLiquidityError(amountAvailableToFillConsideringSlippage);
} }
// if we are not buying ZRX:
// given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
// TODO(bmillman): optimization
// update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
let resultFeeOrders = [] as SignedOrder[];
let feeOrdersRemainingFillableMakerAssetAmounts = [] as BigNumber[];
if (!shouldDisableFeeOrderCalculations && !isMakerAssetZrxToken) {
const feeOrdersAndRemainingFeeAmount = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
resultOrders,
feeOrders,
{
remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
},
);
// if we do not have enough feeOrders to cover the fees, throw
if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(SwapQuoterError.InsufficientZrxLiquidity);
}
resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders;
feeOrdersRemainingFillableMakerAssetAmounts =
feeOrdersAndRemainingFeeAmount.feeOrdersRemainingFillableMakerAssetAmounts;
}
// assetData information for the result // assetData information for the result
const takerAssetData = orders[0].takerAssetData; const takerAssetData = resultOrders[0].takerAssetData;
const makerAssetData = orders[0].makerAssetData; const makerAssetData = resultOrders[0].makerAssetData;
// compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount const bestCaseQuoteInfo = calculateQuoteInfo(resultOrders, assetFillAmount, gasPrice, marketOperation);
const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultOrders,
remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
};
const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultFeeOrders,
remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
};
const bestCaseQuoteInfo = calculateQuoteInfo(
trimmedOrdersAndFillableAmounts,
trimmedFeeOrdersAndFillableAmounts,
assetFillAmount,
isMakerAssetZrxToken,
shouldDisableFeeOrderCalculations,
marketOperation,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = calculateQuoteInfo( const worstCaseQuoteInfo = calculateQuoteInfo(
reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts), _.reverse(_.clone(resultOrders)),
reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
assetFillAmount, assetFillAmount,
isMakerAssetZrxToken, gasPrice,
shouldDisableFeeOrderCalculations,
marketOperation, marketOperation,
); );
@ -187,7 +111,6 @@ function calculateSwapQuote(
takerAssetData, takerAssetData,
makerAssetData, makerAssetData,
orders: resultOrders, orders: resultOrders,
feeOrders: resultFeeOrders,
bestCaseQuoteInfo, bestCaseQuoteInfo,
worstCaseQuoteInfo, worstCaseQuoteInfo,
}; };
@ -208,199 +131,159 @@ function calculateSwapQuote(
} }
function calculateQuoteInfo( function calculateQuoteInfo(
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, assetFillAmount: BigNumber,
tokenAmount: BigNumber, gasPrice: BigNumber,
isMakerAssetZrxToken: boolean, operation: MarketOperation,
shouldDisableFeeOrderCalculations: boolean,
marketOperation: MarketOperation,
): SwapQuoteInfo { ): SwapQuoteInfo {
// find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right if (operation === MarketOperation.Buy) {
let makerTokenAmount = marketOperation === MarketOperation.Buy ? tokenAmount : constants.ZERO_AMOUNT; return calculateMarketBuyQuoteInfo(prunedOrders, assetFillAmount, gasPrice);
let takerTokenAmount = marketOperation === MarketOperation.Sell ? tokenAmount : constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
if (isMakerAssetZrxToken) {
if (marketOperation === MarketOperation.Buy) {
takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makerTokenAmount);
} else {
makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount(
ordersAndFillableAmounts,
takerTokenAmount,
);
}
} else { } else {
const findTokenAndZrxAmount = return calculateMarketSellQuoteInfo(prunedOrders, assetFillAmount, gasPrice);
marketOperation === MarketOperation.Buy
? findTakerTokenAndZrxAmountNeededToBuyAsset
: findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset;
// find eth and zrx amounts needed to buy
const tokenAndZrxAmountToBuyAsset = findTokenAndZrxAmount(
ordersAndFillableAmounts,
marketOperation === MarketOperation.Buy ? makerTokenAmount : takerTokenAmount,
);
if (marketOperation === MarketOperation.Buy) {
takerTokenAmount = tokenAndZrxAmountToBuyAsset[0];
} else {
makerTokenAmount = tokenAndZrxAmountToBuyAsset[0];
}
const zrxAmountToBuyAsset = tokenAndZrxAmountToBuyAsset[1];
// find eth amount needed to buy zrx
zrxTakerTokenAmount = shouldDisableFeeOrderCalculations
? constants.ZERO_AMOUNT
: findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
} }
const feeTakerTokenAmount = zrxTakerTokenAmount;
// eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees
const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount);
return {
makerTokenAmount,
takerTokenAmount,
feeTakerTokenAmount,
totalTakerTokenAmount,
};
}
// given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties
function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts {
const ordersCopy = _.clone(ordersAndFillableAmounts.orders);
const remainingFillableMakerAssetAmountsCopy = _.clone(ordersAndFillableAmounts.remainingFillableMakerAssetAmounts);
return {
orders: ordersCopy.reverse(),
remainingFillableMakerAssetAmounts: remainingFillableMakerAssetAmountsCopy.reverse(),
};
} }
function findZrxTokenAmountFromSellingTakerTokenAmount( function calculateMarketSellQuoteInfo(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
takerAssetSellAmount: BigNumber, takerAssetSellAmount: BigNumber,
): BigNumber { gasPrice: BigNumber,
const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts; ): SwapQuoteInfo {
const result = _.reduce( const result = _.reduce(
orders, prunedOrders,
(acc, order, index) => { (acc, order) => {
const { totalZrxTokenAmount, remainingTakerAssetFillAmount } = acc; const {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; totalMakerAssetAmount,
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount( totalTakerAssetAmount,
order, totalFeeTakerAssetAmount,
remainingFillableMakerAssetAmount, remainingTakerAssetFillAmount,
} = acc;
const [
adjustedFillableMakerAssetAmount,
adjustedFillableTakerAssetAmount,
] = utils.getAdjustedFillableMakerAndTakerAmountsFromTakerFees(order);
const takerAssetAmountWithFees = BigNumber.min(
remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount,
); );
const takerFillAmount = BigNumber.min(remainingTakerAssetFillAmount, remainingFillableTakerAssetAmount); const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
const makerFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerFillAmount); order,
const feeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount); takerAssetAmountWithFees,
);
const makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return { return {
totalZrxTokenAmount: totalZrxTokenAmount.plus(makerFillAmount).minus(feeAmount), totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingTakerAssetFillAmount: BigNumber.max( remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT, constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerFillAmount), remainingTakerAssetFillAmount.minus(takerAssetAmountWithFees),
), ),
}; };
}, },
{ {
totalZrxTokenAmount: constants.ZERO_AMOUNT, totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: takerAssetSellAmount, remainingTakerAssetFillAmount: takerAssetSellAmount,
}, },
); );
return result.totalZrxTokenAmount; return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInEthAmount: protocolFeeUtils.calculateWorstCaseProtocolFee(prunedOrders, gasPrice),
};
} }
function findTakerTokenAmountNeededToBuyZrx( function calculateMarketBuyQuoteInfo(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
zrxBuyAmount: BigNumber,
): BigNumber {
const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalTakerTokenAmount, remainingZrxBuyAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount);
const [takerFillAmount, adjustedMakerFillAmount] = orderCalculationUtils.getTakerFillAmountForFeeOrder(
order,
makerFillAmount,
);
const extraFeeAmount = remainingFillableMakerAssetAmount.isGreaterThanOrEqualTo(adjustedMakerFillAmount)
? constants.ZERO_AMOUNT
: adjustedMakerFillAmount.minus(makerFillAmount);
return {
totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount),
remainingZrxBuyAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingZrxBuyAmount.minus(makerFillAmount).plus(extraFeeAmount),
),
};
},
{
totalTakerTokenAmount: constants.ZERO_AMOUNT,
remainingZrxBuyAmount: zrxBuyAmount,
},
);
return result.totalTakerTokenAmount;
}
function findTakerTokenAndZrxAmountNeededToBuyAsset(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
makerAssetBuyAmount: BigNumber, makerAssetBuyAmount: BigNumber,
): [BigNumber, BigNumber] { gasPrice: BigNumber,
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts; ): SwapQuoteInfo {
const result = _.reduce( const result = _.reduce(
orders, prunedOrders,
(acc, order, index) => { (acc, order) => {
const { totalTakerTokenAmount, totalZrxAmount, remainingmakerAssetFillAmount } = acc; const {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; totalMakerAssetAmount,
const makerFillAmount = BigNumber.min(acc.remainingmakerAssetFillAmount, remainingFillableMakerAssetAmount); totalTakerAssetAmount,
const takerFillAmount = orderCalculationUtils.getTakerFillAmount(order, makerFillAmount); totalFeeTakerAssetAmount,
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount); remainingMakerAssetFillAmount,
} = acc;
const [
adjustedFillableMakerAssetAmount,
adjustedFillableTakerAssetAmount,
] = utils.getAdjustedFillableMakerAndTakerAmountsFromTakerFees(order);
const makerFillAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerFillAmount
.div(adjustedFillableMakerAssetAmount)
.multipliedBy(adjustedFillableTakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
return { return {
totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount), totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount),
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount), totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
remainingmakerAssetFillAmount: BigNumber.max( totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingMakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT, constants.ZERO_AMOUNT,
remainingmakerAssetFillAmount.minus(makerFillAmount), remainingMakerAssetFillAmount.minus(makerFillAmount),
), ),
}; };
}, },
{ {
totalTakerTokenAmount: constants.ZERO_AMOUNT, totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalZrxAmount: constants.ZERO_AMOUNT, totalTakerAssetAmount: constants.ZERO_AMOUNT,
remainingmakerAssetFillAmount: makerAssetBuyAmount, totalFeeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingMakerAssetFillAmount: makerAssetBuyAmount,
}, },
); );
return [result.totalTakerTokenAmount, result.totalZrxAmount]; return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInEthAmount: protocolFeeUtils.calculateWorstCaseProtocolFee(prunedOrders, gasPrice),
};
} }
function findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset( function getTakerAssetAmountBreakDown(
ordersAndFillableAmounts: OrdersAndFillableAmounts, order: PrunedSignedOrder,
takerAssetSellAmount: BigNumber, takerAssetAmountWithFees: BigNumber,
): [BigNumber, BigNumber] { ): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts; if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
const result = _.reduce( const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee);
orders, const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount);
(acc, order, index) => { const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL);
const { totalMakerTokenAmount, totalZrxAmount, remainingTakerAssetFillAmount } = acc; return {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; takerAssetAmount,
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount( feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
order, };
remainingFillableMakerAssetAmount, } else if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
); if (takerAssetAmountWithFees.isZero()) {
const takerFillAmount = BigNumber.min(acc.remainingTakerAssetFillAmount, remainingFillableTakerAssetAmount);
const makerFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerFillAmount);
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
return { return {
totalMakerTokenAmount: totalMakerTokenAmount.plus(makerFillAmount), takerAssetAmount: constants.ZERO_AMOUNT,
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount), feeTakerAssetAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerFillAmount),
),
}; };
}, }
{ const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerAssetAmountWithFees);
totalMakerTokenAmount: constants.ZERO_AMOUNT, const makerAssetFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerAssetAmountWithFees);
totalZrxAmount: constants.ZERO_AMOUNT, const takerAssetAmount = takerFeeAmount
remainingTakerAssetFillAmount: takerAssetSellAmount, .div(makerAssetFillAmount)
}, .multipliedBy(takerAssetAmountWithFees)
); .integerValue(BigNumber.ROUND_CEIL);
return [result.totalMakerTokenAmount, result.totalZrxAmount]; return {
takerAssetAmount,
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
};
}
return {
feeTakerAssetAmount: constants.ZERO_AMOUNT,
takerAssetAmount: takerAssetAmountWithFees,
};
} }

View File

@ -1,5 +1,7 @@
import { ContractWrappers } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { MarketOperation, SignedOrder } from '@0x/types'; import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { WETH9Contract } from '@0x/contracts-erc20';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper'; import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types'; import { Provider } from 'ethereum-types';
@ -47,19 +49,17 @@ export const swapQuoteConsumerUtils = {
}, },
async getEthAndWethBalanceAsync( async getEthAndWethBalanceAsync(
provider: SupportedProvider, provider: SupportedProvider,
contractWrappers: ContractWrappers, contractAddresses: ContractAddresses,
takerAddress: string, takerAddress: string,
): Promise<[BigNumber, BigNumber]> { ): Promise<[BigNumber, BigNumber]> {
const weth = new WETH9Contract(contractAddresses.etherToken, provider);
const web3Wrapper = new Web3Wrapper(provider); const web3Wrapper = new Web3Wrapper(provider);
const ethBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); const ethBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
const wethBalance = await contractWrappers.weth9.balanceOf(takerAddress).callAsync(); const wethBalance = await weth.balanceOf(takerAddress).callAsync();
return [ethBalance, wethBalance]; return [ethBalance, wethBalance];
}, },
isValidForwarderSwapQuote(swapQuote: SwapQuote, wethAssetData: string): boolean { isValidForwarderSwapQuote(swapQuote: SwapQuote, wethAssetData: string): boolean {
return ( return swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData);
swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData) &&
swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.feeOrders, wethAssetData)
);
}, },
isValidForwarderSignedOrders(orders: SignedOrder[], wethAssetData: string): boolean { isValidForwarderSignedOrders(orders: SignedOrder[], wethAssetData: string): boolean {
return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData)); return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData));
@ -67,35 +67,25 @@ export const swapQuoteConsumerUtils = {
isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean { isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean {
return order.takerAssetData === wethAssetData; return order.takerAssetData === wethAssetData;
}, },
optimizeOrdersForMarketExchangeOperation(orders: SignedOrder[], operation: MarketOperation): SignedOrder[] {
return _.map(orders, (order: SignedOrder, index: number) => {
const optimizedOrder = _.clone(order);
if (operation === MarketOperation.Sell && index !== 0) {
optimizedOrder.takerAssetData = constants.NULL_BYTES;
} else if (index !== 0) {
optimizedOrder.makerAssetData = constants.NULL_BYTES;
}
return optimizedOrder;
});
},
async getExtensionContractTypeForSwapQuoteAsync( async getExtensionContractTypeForSwapQuoteAsync(
quote: SwapQuote, quote: SwapQuote,
contractWrappers: ContractWrappers, contractAddresses: ContractAddresses,
provider: Provider, provider: Provider,
opts: Partial<GetExtensionContractTypeOpts>, opts: Partial<GetExtensionContractTypeOpts>,
): Promise<ExtensionContractType> { ): Promise<ExtensionContractType> {
const wethAssetData = await contractWrappers.devUtils const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
.encodeERC20AssetData(contractWrappers.contractAddresses.etherToken) const wethAssetData = await devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync();
.callAsync();
if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) { if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) {
if (opts.takerAddress !== undefined) { if (opts.takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', opts.takerAddress); assert.isETHAddressHex('takerAddress', opts.takerAddress);
} }
const ethAmount = opts.ethAmount || quote.worstCaseQuoteInfo.totalTakerTokenAmount; const ethAmount =
opts.ethAmount ||
quote.worstCaseQuoteInfo.takerAssetAmount.plus(quote.worstCaseQuoteInfo.protocolFeeInEthAmount);
const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts); const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts);
const takerEthAndWethBalance = const takerEthAndWethBalance =
takerAddress !== undefined takerAddress !== undefined
? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractWrappers, takerAddress) ? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractAddresses, takerAddress)
: [constants.ZERO_AMOUNT, constants.ZERO_AMOUNT]; : [constants.ZERO_AMOUNT, constants.ZERO_AMOUNT];
// TODO(david): when considering if there is enough Eth balance, should account for gas costs. // TODO(david): when considering if there is enough Eth balance, should account for gas costs.
const isEnoughEthAndWethBalance = _.map(takerEthAndWethBalance, (balance: BigNumber) => const isEnoughEthAndWethBalance = _.map(takerEthAndWethBalance, (balance: BigNumber) =>

View File

@ -1,11 +1,11 @@
import { SignedOrder } from '@0x/types'; import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types'; import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants } from '../constants'; import { constants } from '../constants';
import { OrdersAndFillableAmounts } from '../types'; import { PrunedSignedOrder } from '../types';
// tslint:disable:no-unnecessary-type-assertion // tslint:disable:no-unnecessary-type-assertion
export const utils = { export const utils = {
@ -27,15 +27,30 @@ export const utils = {
}, },
) as MethodAbi | undefined; ) as MethodAbi | undefined;
}, },
isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts: OrdersAndFillableAmounts): boolean { isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts; return order.takerFeeAssetData === order.makerAssetData;
return _.some( },
orders, isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
(order: SignedOrder, index: number): boolean => { return order.takerFeeAssetData === order.takerAssetData;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index]; },
// If takerFee is a non zero value and order is still fillable, fee orders are required getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
return !order.takerFee.isZero() && !remainingFillableMakerAssetAmount.isZero(); const adjustedMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
}, ? order.makerAssetAmount.minus(order.takerFee)
); : order.makerAssetAmount;
const adjustedTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
? order.takerAssetAmount.plus(order.takerFee)
: order.takerAssetAmount;
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
},
getAdjustedFillableMakerAndTakerAmountsFromTakerFees<T extends PrunedSignedOrder>(
order: T,
): [BigNumber, BigNumber] {
const adjustedFillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
: order.fillableMakerAssetAmount;
const adjustedFillableTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
: order.fillableTakerAssetAmount;
return [adjustedFillableMakerAssetAmount, adjustedFillableTakerAssetAmount];
}, },
}; };

View File

@ -1,92 +0,0 @@
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,57 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { calculateLiquidity } from '../src/utils/calculate_liquidity';
import { chaiSetup } from './utils/chai_setup';
import { testOrders } from './utils/test_orders';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
const {
PRUNED_SIGNED_ORDERS_FEELESS,
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
} = testOrders;
// tslint:disable:custom-no-magic-numbers
describe('#calculateLiquidity', () => {
it('should provide correct liquidity result with feeless orders', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEELESS;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with orders with takerFees in takerAsset', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15));
});
it('should provide correct liquidity result with orders with takerFees in makerAsset', () => {
const prunedSignedOrders = PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(5));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with mixed orders with fees and no fees', () => {
const prunedSignedOrders = _.concat(
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEELESS,
);
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(25));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33));
});
});

View File

@ -1,7 +1,9 @@
import { ContractAddresses, ContractWrappers, ERC20TokenContract } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { ExchangeContract } from '@0x/contracts-exchange';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import 'mocha'; import 'mocha';
@ -13,7 +15,9 @@ import {
ExchangeMarketBuySmartContractParams, ExchangeMarketBuySmartContractParams,
ExchangeMarketSellSmartContractParams, ExchangeMarketSellSmartContractParams,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote, MarketSellSwapQuote,
PrunedSignedOrder,
} from '../src/types'; } from '../src/types';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
@ -25,16 +29,47 @@ chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337; const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const FILLABLE_AMOUNTS = [new BigNumber(3), new BigNumber(2), new BigNumber(5)].map(value => const UNLIMITED_ALLOWANCE = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
value.multipliedBy(ONE_ETH_IN_WEI),
); const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
{
takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const expectMakerAndTakerBalancesAsyncFactory = (
erc20TokenContract: ERC20TokenContract,
makerAddress: string,
takerAddress: string,
) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => {
const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(expectedMakerBalance);
expect(takerBalance).to.bignumber.equal(expectedTakerBalance);
};
describe('ExchangeSwapQuoteConsumer', () => { describe('ExchangeSwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[]; let userAddresses: string[];
let erc20TokenContract: ERC20TokenContract; let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let coinbaseAddress: string; let coinbaseAddress: string;
let makerAddress: string; let makerAddress: string;
let takerAddress: string; let takerAddress: string;
@ -46,32 +81,38 @@ describe('ExchangeSwapQuoteConsumer', () => {
let takerAssetData: string; let takerAssetData: string;
let wethAssetData: string; let wethAssetData: string;
let contractAddresses: ContractAddresses; let contractAddresses: ContractAddresses;
let exchangeContract: ExchangeContract;
const chainId = TESTRPC_CHAIN_ID; const chainId = TESTRPC_CHAIN_ID;
let orders: SignedOrder[]; let orders: PrunedSignedOrder[];
let marketSellSwapQuote: SwapQuote; let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote; let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ExchangeSwapQuoteConsumer; let swapQuoteConsumer: ExchangeSwapQuoteConsumer;
let expectMakerAndTakerBalancesForMakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
let expectMakerAndTakerBalancesForTakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
before(async () => { before(async () => {
contractAddresses = await migrateOnceAsync(); contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync(); userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
chainId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [ const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(), [makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(), devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(), devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
]; devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider); ]);
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
// Configure order defaults // Configure order defaults
const defaultOrderParams = { const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS, ...devConstants.STATIC_ORDER_PARAMS,
@ -79,17 +120,26 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAddress, takerAddress,
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
makerFeeAssetData: await contractWrappers.devUtils makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
.encodeERC20AssetData(contractAddresses.zrxToken) takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
.callAsync(), makerFee: constants.ZERO_AMOUNT,
takerFeeAssetData: await contractWrappers.devUtils takerFee: constants.ZERO_AMOUNT,
.encodeERC20AssetData(contractAddresses.zrxToken) feeRecipientAddress: feeRecipient,
.callAsync(),
exchangeAddress: contractAddresses.exchange, exchangeAddress: contractAddresses.exchange,
chainId, chainId,
}; };
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams); orderFactory = new OrderFactory(privateKey, defaultOrderParams);
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TakerTokenContract,
makerAddress,
takerAddress,
);
expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20MakerTokenContract,
makerAddress,
takerAddress,
);
}); });
after(async () => { after(async () => {
await blockchainLifecycle.revertAsync(); await blockchainLifecycle.revertAsync();
@ -97,12 +147,13 @@ describe('ExchangeSwapQuoteConsumer', () => {
beforeEach(async () => { beforeEach(async () => {
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
orders = []; orders = [];
for (const fillableAmount of FILLABLE_AMOUNTS) { for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await orderFactory.newSignedOrderAsync({ const order = await orderFactory.newSignedOrderAsync(partialOrder);
makerAssetAmount: fillableAmount, const prunedOrder = {
takerAssetAmount: fillableAmount, ...order,
}); ...partialOrder,
orders.push(order); };
orders.push(prunedOrder as PrunedSignedOrder);
} }
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees( marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -110,6 +161,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAssetData, takerAssetData,
orders, orders,
MarketOperation.Sell, MarketOperation.Sell,
GAS_PRICE,
); );
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees( marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -117,45 +169,87 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAssetData, takerAssetData,
orders, orders,
MarketOperation.Buy, MarketOperation.Buy,
GAS_PRICE,
); );
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, { swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
chainId, chainId,
}); });
await erc20MakerTokenContract
.transfer(makerAddress, marketBuySwapQuote.worstCaseQuoteInfo.makerAssetAmount)
.sendTransactionAsync({
from: coinbaseAddress,
});
await erc20TakerTokenContract
.transfer(takerAddress, marketBuySwapQuote.worstCaseQuoteInfo.totalTakerAssetAmount)
.sendTransactionAsync({
from: coinbaseAddress,
});
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: makerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: takerAddress });
}); });
afterEach(async () => { afterEach(async () => {
await blockchainLifecycle.revertAsync(); await blockchainLifecycle.revertAsync();
}); });
describe('executeSwapQuoteOrThrowAsync', () => { describe('#executeSwapQuoteOrThrowAsync', () => {
/* /*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) * 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 * 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 () => { it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress }); await expectMakerAndTakerBalancesForTakerAssetAsync(
makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); constants.ZERO_AMOUNT,
takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); );
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
}); });
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress }); await expectMakerAndTakerBalancesForTakerAssetAsync(
makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); constants.ZERO_AMOUNT,
takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); );
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
}); });
}); });
describe('getSmartContractParamsOrThrow', () => { describe('#getSmartContractParamsOrThrow', () => {
describe('valid swap quote', async () => { describe('valid swap quote', async () => {
// TODO(david) Check for valid MethodAbi // TODO(david) Check for valid MethodAbi
it('provide correct and optimized smart contract params for a marketSell SwapQuote', async () => { it('provide correct and optimized smart contract params for a marketSell SwapQuote', async () => {
@ -163,7 +257,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
marketSellSwapQuote, marketSellSwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.exchange.address); expect(toAddress).to.deep.equal(exchangeContract.address);
const { takerAssetFillAmount, signatures, type } = params as ExchangeMarketSellSmartContractParams; const { takerAssetFillAmount, signatures, type } = params as ExchangeMarketSellSmartContractParams;
expect(type).to.deep.equal(MarketOperation.Sell); expect(type).to.deep.equal(MarketOperation.Sell);
expect(takerAssetFillAmount).to.bignumber.equal( expect(takerAssetFillAmount).to.bignumber.equal(
@ -172,12 +266,12 @@ describe('ExchangeSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature); const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures); expect(signatures).to.deep.equal(orderSignatures);
}); });
it('provide correct and optimized smart contract params for a marketBuy SwapQuote', async () => { it('provide correct smart contract params for a marketBuy SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote, marketBuySwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.exchange.address); expect(toAddress).to.deep.equal(exchangeContract.address);
const { makerAssetFillAmount, signatures, type } = params as ExchangeMarketBuySmartContractParams; const { makerAssetFillAmount, signatures, type } = params as ExchangeMarketBuySmartContractParams;
expect(type).to.deep.equal(MarketOperation.Buy); expect(type).to.deep.equal(MarketOperation.Buy);
expect(makerAssetFillAmount).to.bignumber.equal( expect(makerAssetFillAmount).to.bignumber.equal(
@ -189,49 +283,53 @@ describe('ExchangeSwapQuoteConsumer', () => {
}); });
}); });
describe('getCalldataOrThrow', () => { describe('#getCalldataOrThrow', () => {
describe('valid swap quote', async () => { describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote, marketSellSwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.exchange.address); expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({ await web3Wrapper.sendTransactionAsync({
from: takerAddress, from: takerAddress,
to: toAddress, to: toAddress,
data: calldataHexString, data: calldataHexString,
gas: 4000000, gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
}); });
makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); 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 () => { it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote, marketBuySwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.exchange.address); expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({ await web3Wrapper.sendTransactionAsync({
from: takerAddress, from: takerAddress,
to: toAddress, to: toAddress,
data: calldataHexString, data: calldataHexString,
gas: 4000000, gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
}); });
makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesForMakerAssetAsync(
takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
}); });
}); });
}); });

View File

@ -1,6 +1,9 @@
import { ContractAddresses, ContractWrappers, ERC20TokenContract } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { ForwarderContract } from '@0x/contracts-exchange-forwarder';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import 'mocha'; import 'mocha';
@ -12,28 +15,61 @@ import {
ForwarderMarketBuySmartContractParams, ForwarderMarketBuySmartContractParams,
ForwarderMarketSellSmartContractParams, ForwarderMarketSellSmartContractParams,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation,
PrunedSignedOrder,
} from '../src/types'; } from '../src/types';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate'; import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote'; import { getFullyFillableSwapQuoteWithNoFees } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper'; import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure(); chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337; const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const MARKET_OPERATION = MarketOperation.Sell;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value => const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI), value.multipliedBy(ONE_ETH_IN_WEI),
); );
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const expectMakerAndTakerBalancesAsyncFactory = (
erc20TokenContract: ERC20TokenContract,
makerAddress: string,
takerAddress: string,
) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => {
const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(expectedMakerBalance);
expect(takerBalance).to.bignumber.equal(expectedTakerBalance);
};
describe('ForwarderSwapQuoteConsumer', () => { describe('ForwarderSwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers; const FEE_PERCENTAGE = 0.05;
let erc20Token: ERC20TokenContract;
let userAddresses: string[]; let userAddresses: string[];
let coinbaseAddress: string; let coinbaseAddress: string;
let makerAddress: string; let makerAddress: string;
@ -43,33 +79,68 @@ describe('ForwarderSwapQuoteConsumer', () => {
let takerTokenAddress: string; let takerTokenAddress: string;
let makerAssetData: string; let makerAssetData: string;
let takerAssetData: string; let takerAssetData: string;
let orderFactory: OrderFactory;
let invalidOrderFactory: OrderFactory;
let wethAssetData: string; let wethAssetData: string;
let contractAddresses: ContractAddresses; let contractAddresses: ContractAddresses;
let erc20TokenContract: ERC20TokenContract;
let forwarderContract: ForwarderContract;
let orders: SignedOrder[]; let orders: PrunedSignedOrder[];
let invalidOrders: PrunedSignedOrder[];
let marketSellSwapQuote: SwapQuote; let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote; let marketBuySwapQuote: SwapQuote;
let invalidMarketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ForwarderSwapQuoteConsumer; let swapQuoteConsumer: ForwarderSwapQuoteConsumer;
let erc20ProxyAddress: string; let expectMakerAndTakerBalancesAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
const chainId = TESTRPC_CHAIN_ID; const chainId = TESTRPC_CHAIN_ID;
before(async () => { before(async () => {
contractAddresses = await migrateOnceAsync(); contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync(); userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
chainId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20Token = new ERC20TokenContract(makerTokenAddress, provider); erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
[makerAssetData, takerAssetData, wethAssetData] = [ forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider);
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(), const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(), [makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(), devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
]; devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
]);
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData: wethAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const invalidDefaultOrderParams = {
...defaultOrderParams,
...{
takerAssetData,
},
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TokenContract,
makerAddress,
takerAddress,
);
invalidOrderFactory = new OrderFactory(privateKey, invalidDefaultOrderParams);
}); });
after(async () => { after(async () => {
await blockchainLifecycle.revertAsync(); await blockchainLifecycle.revertAsync();
@ -77,35 +148,48 @@ describe('ForwarderSwapQuoteConsumer', () => {
beforeEach(async () => { beforeEach(async () => {
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS; const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
erc20ProxyAddress = contractAddresses.erc20Proxy;
const totalFillableAmount = FILLABLE_AMOUNTS.reduce( const totalFillableAmount = FILLABLE_AMOUNTS.reduce(
(a: BigNumber, c: BigNumber) => a.plus(c), (a: BigNumber, c: BigNumber) => a.plus(c),
new BigNumber(0), new BigNumber(0),
); );
await erc20Token.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({ await erc20TokenContract.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({
from: coinbaseAddress, from: coinbaseAddress,
}); });
await erc20Token.approve(erc20ProxyAddress, UNLIMITED_ALLOWANCE).sendTransactionAsync({ await erc20TokenContract
from: makerAddress, .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
}); .sendTransactionAsync({ from: makerAddress });
orders = await getSignedOrdersWithNoFeesAsync(
provider, await forwarderContract.approveMakerAssetProxy(makerAssetData).sendTransactionAsync({ from: makerAddress });
makerAssetData,
wethAssetData, orders = [];
makerAddress, for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
takerAddress, const order = await orderFactory.newSignedOrderAsync(partialOrder);
FILLABLE_AMOUNTS, const prunedOrder = {
contractAddresses.exchange, ...order,
); ...partialOrder,
};
orders.push(prunedOrder as PrunedSignedOrder);
}
invalidOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await invalidOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
invalidOrders.push(prunedOrder as PrunedSignedOrder);
}
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees( marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData, makerAssetData,
wethAssetData, wethAssetData,
orders, orders,
MarketOperation.Sell, MarketOperation.Sell,
GAS_PRICE,
); );
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees( marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -113,34 +197,29 @@ describe('ForwarderSwapQuoteConsumer', () => {
wethAssetData, wethAssetData,
orders, orders,
MarketOperation.Buy, MarketOperation.Buy,
GAS_PRICE,
); );
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, { invalidMarketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidOrders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
chainId, chainId,
}); });
}); });
afterEach(async () => { afterEach(async () => {
await blockchainLifecycle.revertAsync(); await blockchainLifecycle.revertAsync();
}); });
describe('executeSwapQuoteOrThrowAsync', () => { describe('#executeSwapQuoteOrThrowAsync', () => {
describe('validation', () => { describe('validation', () => {
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth', async () => { 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( expect(
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidSwapQuote, { takerAddress }), swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }),
).to.be.rejectedWith( ).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
); );
@ -153,89 +232,98 @@ describe('ForwarderSwapQuoteConsumer', () => {
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) * 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 * 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 () => { it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress }); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); takerAddress,
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); gasPrice: GAS_PRICE,
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI)); gasLimit: 4000000,
expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI)); });
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
}); });
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress }); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); takerAddress,
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); gasPrice: GAS_PRICE,
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); gasLimit: 4000000,
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); });
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
}); });
it('should perform a marketBuy execution with affiliate fees', async () => { it('should perform a marketBuy execution with affiliate fees', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); 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, { await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress, takerAddress,
feePercentage: 0.05, gasPrice: GAS_PRICE,
gasLimit: 4000000,
feePercentage: FEE_PERCENTAGE,
feeRecipient, feeRecipient,
}); });
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInEthAmount,
);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI), new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
); );
}); });
// TODO(david) Finish marketSell affiliate fee excution testing it('should perform a marketSell execution with affiliate fees', async () => {
// it('should perform a marketSell execution with affiliate fees', async () => { await expectMakerAndTakerBalancesAsync(
// let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
// let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
// const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); );
// expect(makerBalance).to.bignumber.equal((new BigNumber(10)).multipliedBy(ONE_ETH_IN_WEI)); const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
// expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceBefore); takerAddress,
// await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, feePercentage: 0.05, feeRecipient }); feePercentage: FEE_PERCENTAGE,
// makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); feeRecipient,
// takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); gasPrice: GAS_PRICE,
// const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); gasLimit: 4000000,
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceAfter); });
// expect(makerBalance).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI)); await expectMakerAndTakerBalancesAsync(
// expect(takerBalance).to.bignumber.equal((new BigNumber(9.5)).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
// expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI)); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
// }); );
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInEthAmount,
);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
}); });
}); });
describe('getSmartContractParamsOrThrow', () => { describe('#getSmartContractParamsOrThrow', () => {
describe('validation', () => { describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync( expect(
provider, swapQuoteConsumer.getSmartContractParamsOrThrowAsync(invalidMarketBuySwapQuote, {}),
makerAssetData, ).to.be.rejectedWith(
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}`, `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
); );
}); });
@ -247,9 +335,8 @@ describe('ForwarderSwapQuoteConsumer', () => {
marketSellSwapQuote, marketSellSwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); expect(toAddress).to.deep.equal(forwarderContract.address);
const { const {
feeSignatures,
feePercentage, feePercentage,
feeRecipient: feeRecipientFromParams, feeRecipient: feeRecipientFromParams,
signatures, signatures,
@ -260,17 +347,15 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature); const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures); expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0); 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 () => { it('provide correct and optimized smart contract params with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote, marketBuySwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); expect(toAddress).to.deep.equal(forwarderContract.address);
const { const {
makerAssetFillAmount, makerAssetFillAmount,
feeSignatures,
feePercentage, feePercentage,
feeRecipient: feeRecipientFromParams, feeRecipient: feeRecipientFromParams,
signatures, signatures,
@ -284,7 +369,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature); const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures); expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0); 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 () => { it('provide correct and optimized smart contract params with affiliate fees for a marketSell SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
@ -294,9 +378,8 @@ describe('ForwarderSwapQuoteConsumer', () => {
feeRecipient, feeRecipient,
}, },
); );
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); expect(toAddress).to.deep.equal(forwarderContract.address);
const { const {
feeSignatures,
feePercentage, feePercentage,
feeRecipient: feeRecipientFromParams, feeRecipient: feeRecipientFromParams,
signatures, signatures,
@ -307,7 +390,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature); const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures); expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI)); 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 () => { it('provide correct and optimized smart contract params with affiliate fees for a marketBuy SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
@ -317,10 +399,9 @@ describe('ForwarderSwapQuoteConsumer', () => {
feeRecipient, feeRecipient,
}, },
); );
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); expect(toAddress).to.deep.equal(forwarderContract.address);
const { const {
makerAssetFillAmount, makerAssetFillAmount,
feeSignatures,
feePercentage, feePercentage,
feeRecipient: feeRecipientFromParams, feeRecipient: feeRecipientFromParams,
signatures, signatures,
@ -334,29 +415,14 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature); const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures); expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI)); expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI));
expect(feeSignatures).to.deep.equal([]);
}); });
}); });
}); });
describe('getCalldataOrThrow', () => { describe('#getCalldataOrThrow', () => {
describe('validation', () => { describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync( expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith(
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}`, `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
); );
}); });
@ -364,33 +430,34 @@ describe('ForwarderSwapQuoteConsumer', () => {
describe('valid swap quote', async () => { describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote, marketSellSwapQuote,
{}, {},
); );
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); expect(toAddress).to.deep.equal(forwarderContract.address);
await web3Wrapper.sendTransactionAsync({ await web3Wrapper.sendTransactionAsync({
from: takerAddress, from: takerAddress,
to: toAddress, to: toAddress,
data: calldataHexString, data: calldataHexString,
value: marketSellSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount, value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000, gas: 4000000,
}); });
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI)); new BigNumber(10).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 () => { it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote, marketBuySwapQuote,
{}, {},
); );
@ -399,19 +466,84 @@ describe('ForwarderSwapQuoteConsumer', () => {
from: takerAddress, from: takerAddress,
to: toAddress, to: toAddress,
data: calldataHexString, data: calldataHexString,
value: marketBuySwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount, value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000, gas: 4000000,
}); });
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync(); await expectMakerAndTakerBalancesAsync(
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync(); constants.ZERO_AMOUNT,
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); );
});
it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{
feePercentage: FEE_PERCENTAGE,
feeRecipient,
},
);
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInEthAmount,
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
});
it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{
feePercentage: FEE_PERCENTAGE,
feeRecipient,
},
);
expect(toAddress).to.deep.equal(contractAddresses.forwarder);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInEthAmount,
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
);
}); });
// 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 () => {
// });
}); });
}); });
// tslint:disable-next-line: max-file-line-count
}); });

View File

@ -0,0 +1,374 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { constants } from '../src/constants';
import { marketUtils } from '../src/utils/market_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrders } from './utils/test_orders';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
// tslint:disable: no-unused-expression
describe('marketUtils', () => {
describe('#findOrdersThatCoverTakerAssetFillAmount', () => {
describe('no orders', () => {
it('returns empty and unchanged remainingFillAmount', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
[],
fillAmount,
);
expect(resultOrders).to.be.empty;
expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
});
});
describe('orders do not have fees', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEELESS;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(5);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(3));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders have fees in takerAsset', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(5);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(1));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders are feeless or have fees in takerAsset', () => {
const inputOrders = _.concat(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
);
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(20);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(12);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(20);
const slippageBufferAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(2));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first four orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(16);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1], inputOrders[2], inputOrders[3]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
});
describe('#findOrdersThatCoverMakerAssetFillAmount', () => {
describe('no orders', () => {
it('returns empty and unchanged remainingFillAmount', async () => {
const fillAmount = baseUnitAmount(9);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
[],
fillAmount,
);
expect(resultOrders).to.be.empty;
expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
});
});
describe('orders do not have fees', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEELESS;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(6);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(10);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(2));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(6);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(8);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders have fees in makerAsset', () => {
const inputOrders = testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET;
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(2);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(4);
const slippageBufferAmount = baseUnitAmount(0.5);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(3);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(1));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
describe('orders are feeless or have fees in makerAsset', () => {
const inputOrders = _.concat(
testOrders.PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
testOrders.PRUNED_SIGNED_ORDERS_FEELESS,
);
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
const fillAmount = baseUnitAmount(12);
const slippageBufferAmount = baseUnitAmount(3);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
const fillAmount = baseUnitAmount(12);
const slippageBufferAmount = baseUnitAmount(2);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
const fillAmount = baseUnitAmount(14);
const slippageBufferAmount = baseUnitAmount(4);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
slippageBufferAmount,
);
expect(resultOrders).to.be.deep.equal(inputOrders);
expect(remainingFillAmount).to.be.bignumber.equal(baseUnitAmount(3));
});
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
const fillAmount = baseUnitAmount(1);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
it('returns first four orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
const fillAmount = baseUnitAmount(11);
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
inputOrders,
fillAmount,
);
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1], inputOrders[2], inputOrders[3]]);
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
});
});
});

View File

@ -0,0 +1,336 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { ExchangeContract } from '@0x/contracts-exchange';
import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { OrderPrunerPermittedFeeTypes } from '../src/types';
import { OrderPruner } from '../src/utils/order_prune_utils';
import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
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_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(constants.PROTOCOL_FEE_MULTIPLIER);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
// tslint:disable: no-unused-expression
// tslint:disable: custom-no-magic-numbers
describe('OrderPruner', () => {
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let exchangeContract: ExchangeContract;
let devUtilsContract: DevUtilsContract;
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 orderFactory: OrderFactory;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let orderPruner: OrderPruner;
let nonOpenSignedOrder: SignedOrder;
let expiredOpenSignedOrder: SignedOrder;
let invalidSignatureOpenSignedOrder: SignedOrder;
let fullyFillableOpenSignedOrder: SignedOrder;
let partiallyFilledOpenSignedOrderFeeless: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder;
let filledOpenSignedOrder: SignedOrder;
const chainId = TESTRPC_CHAIN_ID;
const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
devUtilsContract = new DevUtilsContract(contractAddresses.devUtils, provider);
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
nonOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).minus(10),
});
invalidSignatureOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
invalidSignatureOpenSignedOrder.signature = constants.NULL_BYTES;
fullyFillableOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
// give double fillableAmount to maker and taker as buffer
await erc20MakerTokenContract
.transfer(makerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20TakerTokenContract
.transfer(takerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: makerAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeless,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeless.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: takerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: makerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
filledOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(filledOpenSignedOrder, fillableAmount, filledOpenSignedOrder.signature)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]),
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('constructor options', () => {
it('should filter for only feeless orders if options permit only feeless orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([OrderPrunerPermittedFeeTypes.NoFees]),
});
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(partiallyFilledOpenSignedOrderFeeless.signature);
});
it('should filter for only takerFee in takerAsset orders if options permit only takerFee in takerAsset orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]),
});
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
);
});
it('should filter for only makerFee in takerAsset orders if options permit only makerFee orders', async () => {
orderPruner = new OrderPruner(devUtilsContract, {
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]),
});
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
);
});
});
describe('#pruneSignedOrdersAsync', () => {
it('should filter out non open orders', async () => {
const orders = [nonOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out expired orders', async () => {
const orders = [expiredOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out invalid signature orders', async () => {
const orders = [invalidSignatureOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out fully filled orders', async () => {
const orders = [filledOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders).to.be.empty;
});
it('should provide correct pruned signed orders for fully fillable orders', async () => {
const orders = [fullyFillableOpenSignedOrder];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
const prunedOrder = resultPrunedOrders[0];
expect(prunedOrder.fillableMakerAssetAmount).to.bignumber.equal(fillableAmount);
expect(prunedOrder.fillableTakerAssetAmount).to.bignumber.equal(fillableAmount);
});
it('should provide correct pruned signed orders for partially fillable orders', async () => {
const orders = [
partiallyFilledOpenSignedOrderFeeless,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeInMakerAsset,
];
const resultPrunedOrders = await orderPruner.pruneSignedOrdersAsync(orders);
expect(resultPrunedOrders[0].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[0].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[1].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
expect(resultPrunedOrders[2].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[2].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultPrunedOrders[2].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});

View File

@ -0,0 +1,134 @@
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { sortingUtils } from '../src/utils/sorting_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b22222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b11111111111111111111111111111111111111111111111111111111111111111';
describe('sortingUtils', () => {
describe('#sortOrders', () => {
// rate: 2 takerAsset / makerAsset
const testOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(250),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(100),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(50),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(200),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(100),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(150),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(150),
takerAssetAmount: new BigNumber(250),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
it('correctly sorts by fee adjusted rate (feeless orders)', async () => {
const orders = [testOrder1, testOrder2, testOrder3];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]);
});
it('correctly sorts by fee adjusted rate (takerAsset denominated fee orders)', async () => {
const orders = [
testOrderWithFeeInTakerAsset1,
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInTakerAsset3,
];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInTakerAsset1,
testOrderWithFeeInTakerAsset3,
]);
});
it('correctly sorts by fee adjusted rate (makerAsset denominated fee orders)', async () => {
const orders = [
testOrderWithFeeInMakerAsset1,
testOrderWithFeeInMakerAsset2,
testOrderWithFeeInMakerAsset3,
];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInMakerAsset2,
testOrderWithFeeInMakerAsset1,
testOrderWithFeeInMakerAsset3,
]);
});
it('correctly sorts by fee adjusted rate (mixed orders)', async () => {
const orders = [testOrderWithFeeInMakerAsset1, testOrderWithFeeInTakerAsset2, testOrder3];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInMakerAsset1,
testOrder3,
]);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,176 +0,0 @@
import { ContractAddresses, ContractWrappers, ERC20TokenContract } from '@0x/contract-wrappers';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote, SwapQuoteConsumer } from '../src';
import { ExtensionContractType } from '../src/types';
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_CHAIN_ID = 1337;
const FILLABLE_AMOUNTS = [new BigNumber(3), new BigNumber(2), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
describe('SwapQuoteConsumer', () => {
let contractWrappers: ContractWrappers;
let erc20Token: ERC20TokenContract;
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;
const chainId = TESTRPC_CHAIN_ID;
let orders: SignedOrder[];
let marketSellSwapQuote: SwapQuote;
let swapQuoteConsumer: SwapQuoteConsumer;
let erc20ProxyAddress: string;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
chainId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20Token = new ERC20TokenContract(makerTokenAddress, provider);
[makerAssetData, takerAssetData, wethAssetData] = [
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
];
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
erc20ProxyAddress = contractAddresses.erc20Proxy;
const totalFillableAmount = FILLABLE_AMOUNTS.reduce(
(a: BigNumber, c: BigNumber) => a.plus(c),
new BigNumber(0),
);
await erc20Token.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({
from: coinbaseAddress,
});
await erc20Token.approve(erc20ProxyAddress, UNLIMITED_ALLOWANCE).sendTransactionAsync({
from: makerAddress,
});
orders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
contractAddresses.exchange,
);
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Sell,
);
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
chainId,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
// TODO(david): write tests to ensure options work for executeSwapQuote
// 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 an asset swap with Forwarder contract when provided corresponding useExtensionContract option', async () => {
// let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
// let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
// 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, useExtensionContract: ConsumerType.Forwarder });
// makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
// takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
// expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
// expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// });
// it('should perform an asset swap with Exchange contract when provided corresponding useExtensionContract option', async () => {
// let makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync();
// let takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
// 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 erc20TokenContract.balanceOf(makerAddress).callAsync();
// takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync();
// 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('should provide correct and optimized smart contract params for Forwarder contract when provided corresponding useExtensionContract option', async () => {
const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
useExtensionContract: ExtensionContractType.Forwarder,
});
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
});
it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useExtensionContract option', async () => {
const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
useExtensionContract: ExtensionContractType.None,
});
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
});
});
});
describe('getCalldataOrThrow', () => {
describe('valid swap quote', async () => {
it('should provide correct and optimized calldata options for Forwarder contract when provided corresponding useExtensionContract option', async () => {
const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
useExtensionContract: ExtensionContractType.Forwarder,
});
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
});
it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useExtensionContract option', async () => {
const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
useExtensionContract: ExtensionContractType.None,
});
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
});
});
});
});

View File

@ -1,16 +1,19 @@
import { ContractAddresses, ContractWrappers } from '@0x/contract-wrappers'; import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { WETH9Contract } from '@0x/contracts-erc20';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import 'mocha'; import 'mocha';
import { SwapQuote, SwapQuoteConsumer } from '../src'; import { SwapQuote, SwapQuoteConsumer } from '../src';
import { ExtensionContractType } from '../src/types'; import { constants } from '../src/constants';
import { ExtensionContractType, MarketOperation, PrunedSignedOrder } from '../src/types';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate'; import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote'; import { getFullyFillableSwapQuoteWithNoFees } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper'; import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure(); chaiSetup.configure();
@ -19,15 +22,52 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337; const TESTRPC_CHAIN_ID = 1337;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value => const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
value.multipliedBy(ONE_ETH_IN_WEI),
); const PARTIAL_PRUNED_SIGNED_ORDERS: Array<Partial<PrunedSignedOrder>> = [
const LARGE_FILLABLE_AMOUNTS = [new BigNumber(20), new BigNumber(20), new BigNumber(20)].map(value => {
value.multipliedBy(ONE_ETH_IN_WEI), takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
); makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<PrunedSignedOrder>> = [
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
];
describe('swapQuoteConsumerUtils', () => { describe('swapQuoteConsumerUtils', () => {
let contractWrappers: ContractWrappers; let wethContract: WETH9Contract;
let userAddresses: string[]; let userAddresses: string[];
let makerAddress: string; let makerAddress: string;
let takerAddress: string; let takerAddress: string;
@ -38,25 +78,48 @@ describe('swapQuoteConsumerUtils', () => {
let wethAssetData: string; let wethAssetData: string;
let contractAddresses: ContractAddresses; let contractAddresses: ContractAddresses;
let swapQuoteConsumer: SwapQuoteConsumer; let swapQuoteConsumer: SwapQuoteConsumer;
let orderFactory: OrderFactory;
let forwarderOrderFactory: OrderFactory;
const chainId = TESTRPC_CHAIN_ID; const chainId = TESTRPC_CHAIN_ID;
before(async () => { before(async () => {
contractAddresses = await migrateOnceAsync(); contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync(); userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = { const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
chainId, wethContract = new WETH9Contract(contractAddresses.etherToken, provider);
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
[takerAddress, makerAddress] = userAddresses; [takerAddress, makerAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [ [makerAssetData, takerAssetData, wethAssetData] = [
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(), await devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(), await devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(), await devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
]; ];
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: constants.NULL_ADDRESS,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const defaultForwarderOrderParams = {
...defaultOrderParams,
...{
takerAssetData: wethAssetData,
},
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
swapQuoteConsumer = new SwapQuoteConsumer(provider, { swapQuoteConsumer = new SwapQuoteConsumer(provider, {
chainId, chainId,
}); });
@ -72,46 +135,50 @@ describe('swapQuoteConsumerUtils', () => {
}); });
describe('getConsumerTypeForSwapQuoteAsync', () => { describe('getConsumerTypeForSwapQuoteAsync', () => {
let forwarderOrders: SignedOrder[]; let forwarderOrders: PrunedSignedOrder[];
let exchangeOrders: SignedOrder[]; let exchangeOrders: PrunedSignedOrder[];
let largeForwarderOrders: SignedOrder[]; let largeForwarderOrders: PrunedSignedOrder[];
let forwarderSwapQuote: SwapQuote; let forwarderSwapQuote: SwapQuote;
let exchangeSwapQuote: SwapQuote; let exchangeSwapQuote: SwapQuote;
let largeForwarderSwapQuote: SwapQuote; let largeForwarderSwapQuote: SwapQuote;
beforeEach(async () => { beforeEach(async () => {
exchangeOrders = await getSignedOrdersWithNoFeesAsync( exchangeOrders = [];
provider, for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
makerAssetData, const order = await orderFactory.newSignedOrderAsync(partialOrder);
takerAssetData, const prunedOrder = {
makerAddress, ...order,
takerAddress, ...partialOrder,
FILLABLE_AMOUNTS, };
); exchangeOrders.push(prunedOrder as PrunedSignedOrder);
}
forwarderOrders = await getSignedOrdersWithNoFeesAsync( forwarderOrders = [];
provider, for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
makerAssetData, const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
wethAssetData, const prunedOrder = {
makerAddress, ...order,
takerAddress, ...partialOrder,
FILLABLE_AMOUNTS, };
); forwarderOrders.push(prunedOrder as PrunedSignedOrder);
}
largeForwarderOrders = await getSignedOrdersWithNoFeesAsync( largeForwarderOrders = [];
provider, for (const partialOrder of PARTIAL_LARGE_PRUNED_SIGNED_ORDERS) {
makerAssetData, const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
wethAssetData, const prunedOrder = {
makerAddress, ...order,
takerAddress, ...partialOrder,
LARGE_FILLABLE_AMOUNTS, };
); largeForwarderOrders.push(prunedOrder as PrunedSignedOrder);
}
forwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees( forwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData, makerAssetData,
wethAssetData, wethAssetData,
forwarderOrders, forwarderOrders,
MarketOperation.Sell, MarketOperation.Sell,
GAS_PRICE,
); );
largeForwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees( largeForwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -119,6 +186,7 @@ describe('swapQuoteConsumerUtils', () => {
wethAssetData, wethAssetData,
largeForwarderOrders, largeForwarderOrders,
MarketOperation.Sell, MarketOperation.Sell,
GAS_PRICE,
); );
exchangeSwapQuote = getFullyFillableSwapQuoteWithNoFees( exchangeSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -126,6 +194,7 @@ describe('swapQuoteConsumerUtils', () => {
takerAssetData, takerAssetData,
exchangeOrders, exchangeOrders,
MarketOperation.Sell, MarketOperation.Sell,
GAS_PRICE,
); );
}); });
@ -145,7 +214,7 @@ describe('swapQuoteConsumerUtils', () => {
}); });
it('should return exchange consumer if takerAsset is wEth and taker has enough weth', async () => { 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); const etherInWei = new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI);
await contractWrappers.weth9.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress }); await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress });
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
forwarderSwapQuote, forwarderSwapQuote,
{ takerAddress }, { takerAddress },
@ -154,7 +223,7 @@ describe('swapQuoteConsumerUtils', () => {
}); });
it('should return forwarder consumer if takerAsset is wEth and takerAddress has no available balance in either weth or eth (defaulting behavior)', async () => { 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); const etherInWei = new BigNumber(50).multipliedBy(ONE_ETH_IN_WEI);
await contractWrappers.weth9.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress }); await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress });
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync( const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
largeForwarderSwapQuote, largeForwarderSwapQuote,
{ takerAddress }, { takerAddress },

View File

@ -1,19 +1,19 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { Orderbook } from '@0x/orderbook'; import { Orderbook } from '@0x/orderbook';
import { Web3ProviderEngine } from '@0x/subproviders'; import { Web3ProviderEngine } from '@0x/subproviders';
import { AssetPairsItem, SignedOrder } from '@0x/types'; import { AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai'; import * as chai from 'chai';
import 'mocha'; import 'mocha';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../src'; import { SwapQuoter } from '../src';
import { constants } from '../src/constants'; import { constants } from '../src/constants';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../src/types'; import { LiquidityForTakerMakerAssetDataPair, PrunedSignedOrder } from '../src/types';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
import { mockAvailableAssetDatas, mockedSwapQuoterWithOrdersAndFillableAmounts, orderbookMock } from './utils/mocks'; import { mockAvailableAssetDatas, mockedSwapQuoterWithPrunedSignedOrders, orderbookMock } from './utils/mocks';
import { testOrderFactory } from './utils/test_order_factory';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure(); chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
@ -27,10 +27,6 @@ const WETH_ASSET_DATA = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5
const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS; const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS;
const ZERO = new BigNumber(0); const ZERO = new BigNumber(0);
const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => {
return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals);
};
const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string): AssetPairsItem[] => { const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string): AssetPairsItem[] => {
const defaultAssetPairItem = { const defaultAssetPairItem = {
minAmount: ZERO, minAmount: ZERO,
@ -64,15 +60,15 @@ const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string):
const expectLiquidityResult = async ( const expectLiquidityResult = async (
web3Provider: Web3ProviderEngine, web3Provider: Web3ProviderEngine,
orderbook: Orderbook, orderbook: Orderbook,
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
expectedLiquidityResult: LiquidityForAssetData, expectedLiquidityResult: LiquidityForTakerMakerAssetDataPair,
) => { ) => {
const mockedSwapQuoter = mockedSwapQuoterWithOrdersAndFillableAmounts( const mockedSwapQuoter = mockedSwapQuoterWithPrunedSignedOrders(
web3Provider, web3Provider,
orderbook, orderbook,
FAKE_MAKER_ASSET_DATA, FAKE_MAKER_ASSET_DATA,
WETH_ASSET_DATA, WETH_ASSET_DATA,
ordersAndFillableAmounts, prunedOrders,
); );
const liquidityResult = await mockedSwapQuoter.object.getLiquidityForMakerTakerAssetDataPairAsync( const liquidityResult = await mockedSwapQuoter.object.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA, FAKE_MAKER_ASSET_DATA,
@ -130,8 +126,8 @@ describe('SwapQuoter', () => {
FAKE_TAKER_ASSET_DATA, FAKE_TAKER_ASSET_DATA,
); );
expect(liquidityResult).to.deep.equal({ expect(liquidityResult).to.deep.equal({
makerTokensAvailableInBaseUnits: new BigNumber(0), makerAssetAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0), takerAssetAvailableInBaseUnits: new BigNumber(0),
}); });
}); });
@ -144,20 +140,15 @@ describe('SwapQuoter', () => {
FAKE_TAKER_ASSET_DATA, FAKE_TAKER_ASSET_DATA,
); );
expect(liquidityResult).to.deep.equal({ expect(liquidityResult).to.deep.equal({
makerTokensAvailableInBaseUnits: new BigNumber(0), makerAssetAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0), takerAssetAvailableInBaseUnits: new BigNumber(0),
}); });
}); });
}); });
describe('assetData is supported', () => { describe('assetData is supported', () => {
// orders // orders
const sellTwoTokensFor1Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({ const sellTenTokensFor10Weth: SignedOrder = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: baseUnitAmount(2),
takerAssetAmount: baseUnitAmount(1, WETH_DECIMALS),
chainId: 42,
});
const sellTenTokensFor10Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({
makerAssetAmount: baseUnitAmount(10), makerAssetAmount: baseUnitAmount(10),
takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS), takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS),
chainId: 42, chainId: 42,
@ -168,98 +159,145 @@ describe('SwapQuoter', () => {
}); });
it('should return 0s when no orders available', async () => { it('should return 0s when no orders available', async () => {
const ordersAndFillableAmounts: OrdersAndFillableAmounts = { const prunedOrders: PrunedSignedOrder[] = [];
orders: [],
remainingFillableMakerAssetAmounts: [],
};
const expectedResult = { const expectedResult = {
makerTokensAvailableInBaseUnits: new BigNumber(0), makerAssetAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0), takerAssetAvailableInBaseUnits: new BigNumber(0),
}; };
await expectLiquidityResult( await expectLiquidityResult(
mockWeb3Provider.object, mockWeb3Provider.object,
mockOrderbook.object, mockOrderbook.object,
ordersAndFillableAmounts, prunedOrders,
expectedResult, expectedResult,
); );
}); });
it('should return correct computed value when orders provided with full fillableAmounts', async () => { it('should return correct computed value when orders provided with full fillableAmounts', async () => {
const orders: SignedOrder[] = [sellTwoTokensFor1Weth, sellTenTokensFor10Weth]; const prunedOrders: PrunedSignedOrder[] = [
const ordersAndFillableAmounts = { {
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth], ...sellTenTokensFor10Weth,
remainingFillableMakerAssetAmounts: orders.map(o => o.makerAssetAmount), ...{
}; fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount,
fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount,
const expectedMakerTokensAvailable = orders[0].makerAssetAmount.plus(orders[1].makerAssetAmount); fillableTakerFeeAmount: constants.ZERO_AMOUNT,
const expectedTakerTokensAvailable = orders[0].takerAssetAmount.plus(orders[1].takerAssetAmount); },
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount,
fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedMakerAssetAvailable = prunedOrders[0].makerAssetAmount.plus(
prunedOrders[1].makerAssetAmount,
);
const expectedTakerAssetAvailable = prunedOrders[0].takerAssetAmount.plus(
prunedOrders[1].takerAssetAmount,
);
const expectedResult = { const expectedResult = {
makerTokensAvailableInBaseUnits: expectedMakerTokensAvailable, makerAssetAvailableInBaseUnits: expectedMakerAssetAvailable,
takerTokensAvailableInBaseUnits: expectedTakerTokensAvailable, takerAssetAvailableInBaseUnits: expectedTakerAssetAvailable,
}; };
await expectLiquidityResult( await expectLiquidityResult(
mockWeb3Provider.object, mockWeb3Provider.object,
mockOrderbook.object, mockOrderbook.object,
ordersAndFillableAmounts, prunedOrders,
expectedResult, expectedResult,
); );
}); });
it('should return correct computed value with one partial fillableAmounts', async () => { it('should return correct computed value with one partial fillableAmounts', async () => {
const ordersAndFillableAmounts = { const prunedOrders: PrunedSignedOrder[] = [
orders: [sellTwoTokensFor1Weth], {
remainingFillableMakerAssetAmounts: [baseUnitAmount(1)], ...sellTenTokensFor10Weth,
}; ...{
fillableMakerAssetAmount: baseUnitAmount(1),
fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = { const expectedResult = {
makerTokensAvailableInBaseUnits: baseUnitAmount(1), makerAssetAvailableInBaseUnits: baseUnitAmount(1),
takerTokensAvailableInBaseUnits: baseUnitAmount(0.5, WETH_DECIMALS), takerAssetAvailableInBaseUnits: baseUnitAmount(0.5, WETH_DECIMALS),
}; };
await expectLiquidityResult( await expectLiquidityResult(
mockWeb3Provider.object, mockWeb3Provider.object,
mockOrderbook.object, mockOrderbook.object,
ordersAndFillableAmounts, prunedOrders,
expectedResult, expectedResult,
); );
}); });
it('should return correct computed value with multiple orders and fillable amounts', async () => { it('should return correct computed value with multiple orders and fillable amounts', async () => {
const ordersAndFillableAmounts = { const prunedOrders: PrunedSignedOrder[] = [
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth], {
remainingFillableMakerAssetAmounts: [baseUnitAmount(1), baseUnitAmount(3)], ...sellTenTokensFor10Weth,
}; ...{
fillableMakerAssetAmount: baseUnitAmount(1),
fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: baseUnitAmount(3),
fillableTakerAssetAmount: baseUnitAmount(3, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = { const expectedResult = {
makerTokensAvailableInBaseUnits: baseUnitAmount(4), makerAssetAvailableInBaseUnits: baseUnitAmount(4),
takerTokensAvailableInBaseUnits: baseUnitAmount(3.5, WETH_DECIMALS), takerAssetAvailableInBaseUnits: baseUnitAmount(3.5, WETH_DECIMALS),
}; };
await expectLiquidityResult( await expectLiquidityResult(
mockWeb3Provider.object, mockWeb3Provider.object,
mockOrderbook.object, mockOrderbook.object,
ordersAndFillableAmounts, prunedOrders,
expectedResult, expectedResult,
); );
}); });
it('should return 0s when no amounts fillable', async () => { it('should return 0s when no amounts fillable', async () => {
const ordersAndFillableAmounts = { const prunedOrders: PrunedSignedOrder[] = [
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth], {
remainingFillableMakerAssetAmounts: [baseUnitAmount(0), baseUnitAmount(0)], ...sellTenTokensFor10Weth,
}; ...{
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = { const expectedResult = {
makerTokensAvailableInBaseUnits: baseUnitAmount(0), makerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT,
takerTokensAvailableInBaseUnits: baseUnitAmount(0, WETH_DECIMALS), takerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT,
}; };
await expectLiquidityResult( await expectLiquidityResult(
mockWeb3Provider.object, mockWeb3Provider.object,
mockOrderbook.object, mockOrderbook.object,
ordersAndFillableAmounts, prunedOrders,
expectedResult, expectedResult,
); );
}); });

View File

@ -4,7 +4,7 @@ import { APIOrder, AssetPairsItem, SignedOrder } from '@0x/types';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../../src/swap_quoter'; import { SwapQuoter } from '../../src/swap_quoter';
import { OrdersAndFillableAmounts } from '../../src/types'; import { PrunedSignedOrder } from '../../src/types';
class OrderbookClass extends Orderbook { class OrderbookClass extends Orderbook {
// tslint:disable-next-line:prefer-function-over-method // tslint:disable-next-line:prefer-function-over-method
@ -49,26 +49,26 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde
return mockedSwapQuoter; return mockedSwapQuoter;
}; };
const mockGetOrdersAndAvailableAmounts = ( const mockGetPrunedSignedOrdersAsync = (
mockedSwapQuoter: TypeMoq.IMock<SwapQuoter>, mockedSwapQuoter: TypeMoq.IMock<SwapQuoter>,
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
): void => { ): void => {
mockedSwapQuoter mockedSwapQuoter
.setup(async a => a.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData)) .setup(async a => a.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData))
.returns(async () => Promise.resolve(ordersAndFillableAmounts)) .returns(async () => Promise.resolve(prunedOrders))
.verifiable(TypeMoq.Times.once()); .verifiable(TypeMoq.Times.once());
}; };
export const mockedSwapQuoterWithOrdersAndFillableAmounts = ( export const mockedSwapQuoterWithPrunedSignedOrders = (
provider: Web3ProviderEngine, provider: Web3ProviderEngine,
orderbook: Orderbook, orderbook: Orderbook,
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
ordersAndFillableAmounts: OrdersAndFillableAmounts, prunedOrders: PrunedSignedOrder[],
): TypeMoq.IMock<SwapQuoter> => { ): TypeMoq.IMock<SwapQuoter> => {
const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook); const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook);
mockGetOrdersAndAvailableAmounts(mockedAssetQuoter, makerAssetData, takerAssetData, ordersAndFillableAmounts); mockGetPrunedSignedOrdersAsync(mockedAssetQuoter, makerAssetData, takerAssetData, prunedOrders);
return mockedAssetQuoter; return mockedAssetQuoter;
}; };

View File

@ -1,134 +1,40 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory'; import { SignedOrder } from '@0x/types';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { SupportedProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants } from '../../src/constants'; import { constants } from '../../src/constants';
import { SwapQuote } from '../../src/types'; import { MarketOperation, PrunedSignedOrder, SwapQuote } from '../../src/types';
import { protocolFeeUtils } from '../../src/utils/protocol_fee_utils';
const ZERO_BIG_NUMBER = new BigNumber(0);
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,
takerAddress: string,
fillableAmounts: BigNumber[],
): SignedOrder[] => {
return _.map(fillableAmounts, (fillableAmount: BigNumber) =>
orderFactory.createSignedOrderFromPartial({
makerAddress,
makerAssetAmount: fillableAmount,
makerAssetData,
takerAssetAmount: fillableAmount,
takerAssetData,
chainId: 42,
}),
);
};
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] },
chainId: 42,
}),
);
};
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 = ( export const getFullyFillableSwapQuoteWithNoFees = (
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
orders: SignedOrder[], orders: PrunedSignedOrder[],
operation: MarketOperation, operation: MarketOperation,
gasPrice: BigNumber,
): SwapQuote => { ): SwapQuote => {
const makerAssetFillAmount = _.reduce( const makerAssetFillAmount = _.reduce(
orders, orders,
(a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount), (a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount),
ZERO_BIG_NUMBER, constants.ZERO_AMOUNT,
); );
const totalTakerTokenAmount = _.reduce( const totalTakerAssetAmount = _.reduce(
orders, orders,
(a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount), (a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount),
ZERO_BIG_NUMBER, constants.ZERO_AMOUNT,
); );
const quoteInfo = { const quoteInfo = {
makerTokenAmount: makerAssetFillAmount, makerAssetAmount: makerAssetFillAmount,
takerTokenAmount: totalTakerTokenAmount, feeTakerAssetAmount: constants.ZERO_AMOUNT,
feeTakerTokenAmount: ZERO_BIG_NUMBER, takerAssetAmount: totalTakerAssetAmount,
totalTakerTokenAmount, totalTakerAssetAmount,
protocolFeeInEthAmount: protocolFeeUtils.calculateWorstCaseProtocolFee(orders, gasPrice),
}; };
const quoteBase = { const quoteBase = {
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,
orders, orders,
feeOrders: [],
bestCaseQuoteInfo: quoteInfo, bestCaseQuoteInfo: quoteInfo,
worstCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo,
}; };
@ -143,7 +49,7 @@ export const getFullyFillableSwapQuoteWithNoFees = (
return { return {
...quoteBase, ...quoteBase,
type: MarketOperation.Sell, type: MarketOperation.Sell,
takerAssetFillAmount: totalTakerTokenAmount, takerAssetFillAmount: totalTakerAssetAmount,
}; };
} }
}; };

View File

@ -0,0 +1,63 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { Order, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { constants } from '../../src/constants';
import { PrunedSignedOrder } from '../../src/types';
const CHAIN_ID = 1337;
const BASE_TEST_ORDER: Order = orderFactory.createOrder(
constants.NULL_ADDRESS,
constants.ZERO_AMOUNT,
constants.NULL_ERC20_ASSET_DATA,
constants.ZERO_AMOUNT,
constants.NULL_ERC20_ASSET_DATA,
constants.NULL_ADDRESS,
CHAIN_ID,
);
const BASE_TEST_SIGNED_ORDER: SignedOrder = {
...BASE_TEST_ORDER,
signature: constants.NULL_BYTES,
};
const BASE_TEST_PRUNED_SIGNED_ORDER: PrunedSignedOrder = {
...BASE_TEST_SIGNED_ORDER,
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
};
export const testOrderFactory = {
generateTestSignedOrder(partialOrder: Partial<SignedOrder>): SignedOrder {
return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder);
},
generateIdenticalTestSignedOrders(partialOrder: Partial<SignedOrder>, numOrders: number): SignedOrder[] {
const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER);
return _.map(baseTestOrders, order => transformObject(order, partialOrder));
},
generateTestSignedOrders(partialOrders: Array<Partial<SignedOrder>>): SignedOrder[] {
return _.map(partialOrders, partialOrder => transformObject(BASE_TEST_SIGNED_ORDER, partialOrder));
},
generateTestPrunedSignedOrder(partialOrder: Partial<PrunedSignedOrder>): PrunedSignedOrder {
return transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder);
},
generateIdenticalTestPrunedSignedOrders(
partialOrder: Partial<PrunedSignedOrder>,
numOrders: number,
): PrunedSignedOrder[] {
const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_PRUNED_SIGNED_ORDER);
return _.map(baseTestOrders, (baseOrder): PrunedSignedOrder => transformObject(baseOrder, partialOrder));
},
generateTestPrunedSignedOrders(partialOrders: Array<Partial<PrunedSignedOrder>>): PrunedSignedOrder[] {
return _.map(
partialOrders,
(partialOrder): PrunedSignedOrder => transformObject(BASE_TEST_PRUNED_SIGNED_ORDER, partialOrder),
);
},
};
function transformObject<T>(input: T, transformation: Partial<T>): T {
const copy = _.cloneDeep(input);
return _.assign(copy, transformation);
}

View File

@ -0,0 +1,146 @@
import { PrunedSignedOrder } from '../../src/types';
import { testOrderFactory } from './test_order_factory';
import { baseUnitAmount } from './utils';
// tslint:disable:custom-no-magic-numbers
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b22222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b11111111111111111111111111111111111111111111111111111111111111111';
const PARTIAL_ORDER: Partial<PrunedSignedOrder> = {
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
};
const PARTIAL_ORDER_FEE_IN_TAKER_ASSET: Partial<PrunedSignedOrder> = {
...{
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
},
...PARTIAL_ORDER,
};
const PARTIAL_ORDER_FEE_IN_MAKER_ASSET: Partial<PrunedSignedOrder> = {
...{
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
},
...PARTIAL_ORDER,
};
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<PrunedSignedOrder>> = [
{
...{
takerAssetAmount: baseUnitAmount(1),
makerAssetAmount: baseUnitAmount(6),
fillableTakerAssetAmount: baseUnitAmount(1),
fillableMakerAssetAmount: baseUnitAmount(6),
},
...PARTIAL_ORDER,
},
{
...{
takerAssetAmount: baseUnitAmount(10),
makerAssetAmount: baseUnitAmount(4),
fillableTakerAssetAmount: baseUnitAmount(5),
fillableMakerAssetAmount: baseUnitAmount(2),
},
...PARTIAL_ORDER,
},
{
...{
takerAssetAmount: baseUnitAmount(6),
makerAssetAmount: baseUnitAmount(6),
fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2),
},
...PARTIAL_ORDER,
},
];
const PARTIAL_PRUNED_SIGNED_FEE_IN_TAKER_ASSET: Array<Partial<PrunedSignedOrder>> = [
{
...{
takerAssetAmount: baseUnitAmount(1),
makerAssetAmount: baseUnitAmount(6),
takerFee: baseUnitAmount(3),
fillableTakerAssetAmount: baseUnitAmount(1),
fillableMakerAssetAmount: baseUnitAmount(6),
fillableTakerFeeAmount: baseUnitAmount(3),
},
...PARTIAL_ORDER_FEE_IN_TAKER_ASSET,
},
{
...{
takerAssetAmount: baseUnitAmount(10),
makerAssetAmount: baseUnitAmount(4),
takerFee: baseUnitAmount(2),
fillableTakerAssetAmount: baseUnitAmount(5),
fillableMakerAssetAmount: baseUnitAmount(2),
fillableTakerFeeAmount: baseUnitAmount(1),
},
...PARTIAL_ORDER_FEE_IN_TAKER_ASSET,
},
{
...{
takerAssetAmount: baseUnitAmount(6),
makerAssetAmount: baseUnitAmount(6),
takerFee: baseUnitAmount(4),
fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2),
fillableTakerFeeAmount: baseUnitAmount(2),
},
...PARTIAL_ORDER_FEE_IN_TAKER_ASSET,
},
];
const PARTIAL_PRUNED_SIGNED_FEE_IN_MAKER_ASSET: Array<Partial<PrunedSignedOrder>> = [
{
...{
takerAssetAmount: baseUnitAmount(5),
makerAssetAmount: baseUnitAmount(2),
takerFee: baseUnitAmount(1),
fillableTakerAssetAmount: baseUnitAmount(5),
fillableMakerAssetAmount: baseUnitAmount(2),
fillableTakerFeeAmount: baseUnitAmount(1),
},
...PARTIAL_ORDER_FEE_IN_MAKER_ASSET,
},
{
...{
takerAssetAmount: baseUnitAmount(2),
makerAssetAmount: baseUnitAmount(12),
takerFee: baseUnitAmount(6),
fillableTakerAssetAmount: baseUnitAmount(1),
fillableMakerAssetAmount: baseUnitAmount(6),
fillableTakerFeeAmount: baseUnitAmount(3),
},
...PARTIAL_ORDER_FEE_IN_MAKER_ASSET,
},
{
...{
takerAssetAmount: baseUnitAmount(3),
makerAssetAmount: baseUnitAmount(3),
takerFee: baseUnitAmount(2),
fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(3),
fillableTakerFeeAmount: baseUnitAmount(2),
},
...PARTIAL_ORDER_FEE_IN_MAKER_ASSET,
},
];
const PRUNED_SIGNED_ORDERS_FEELESS = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS,
);
const PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_FEE_IN_TAKER_ASSET,
);
const PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET = testOrderFactory.generateTestPrunedSignedOrders(
PARTIAL_PRUNED_SIGNED_FEE_IN_MAKER_ASSET,
);
export const testOrders = {
PRUNED_SIGNED_ORDERS_FEELESS,
PRUNED_SIGNED_ORDERS_FEE_IN_TAKER_ASSET,
PRUNED_SIGNED_ORDERS_FEE_IN_MAKER_ASSET,
};

View File

@ -0,0 +1,9 @@
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
const TOKEN_DECIMALS = 18;
// tslint:disable:custom-no-magic-numbers
export const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => {
return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals);
};

View File

@ -1,82 +0,0 @@
import { orderFactory } from '@0x/order-utils/lib/src/order_factory';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { utils } from '../src/utils/utils';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const TOKEN_DECIMALS = 18;
const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS;
const baseUnitAmount = (unitAmount: number, decimals = TOKEN_DECIMALS): BigNumber => {
return Web3Wrapper.toBaseUnitAmount(new BigNumber(unitAmount), decimals);
};
// tslint:disable:custom-no-magic-numbers
describe('utils', () => {
// orders
const sellTwoTokensFor1Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({
chainId: 42,
makerAssetAmount: baseUnitAmount(2),
takerAssetAmount: baseUnitAmount(1, WETH_DECIMALS),
});
const sellTenTokensFor10Weth: SignedOrder = orderFactory.createSignedOrderFromPartial({
chainId: 42,
makerAssetAmount: baseUnitAmount(10),
takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS),
});
const sellTwoTokensFor1WethWithTwoTokenFee: SignedOrder = orderFactory.createSignedOrderFromPartial({
chainId: 42,
makerAssetAmount: baseUnitAmount(2),
takerAssetAmount: baseUnitAmount(1, WETH_DECIMALS),
takerFee: baseUnitAmount(2),
});
const sellTenTokensFor1WethWithFourTokenFee: SignedOrder = orderFactory.createSignedOrderFromPartial({
chainId: 42,
makerAssetAmount: baseUnitAmount(2),
takerAssetAmount: baseUnitAmount(1, WETH_DECIMALS),
takerFee: baseUnitAmount(4),
});
describe('isFeeOrdersRequiredToFillOrders', async () => {
it('should return true if ordersAndFillableAmounts is completed unfilled and has fees', () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1WethWithTwoTokenFee, sellTenTokensFor1WethWithFourTokenFee],
remainingFillableMakerAssetAmounts: [baseUnitAmount(1), baseUnitAmount(10)],
};
const isFeeOrdersRequired = utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts);
expect(isFeeOrdersRequired).to.equal(true);
});
it('should return true if ordersAndFillableAmounts is partially unfilled and has fees', () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1WethWithTwoTokenFee, sellTenTokensFor1WethWithFourTokenFee],
remainingFillableMakerAssetAmounts: [baseUnitAmount(0), baseUnitAmount(5)],
};
const isFeeOrdersRequired = utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts);
expect(isFeeOrdersRequired).to.equal(true);
});
it('should return false if ordersAndFillableAmounts is completed filled and has fees', () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1WethWithTwoTokenFee, sellTenTokensFor1WethWithFourTokenFee],
remainingFillableMakerAssetAmounts: [baseUnitAmount(0), baseUnitAmount(0)],
};
const isFeeOrdersRequired = utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts);
expect(isFeeOrdersRequired).to.equal(false);
});
it("should return false if ordersAndFillableAmounts is completely unfilled and doesn't have fees", () => {
const ordersAndFillableAmounts = {
orders: [sellTwoTokensFor1Weth, sellTenTokensFor10Weth],
remainingFillableMakerAssetAmounts: [baseUnitAmount(1), baseUnitAmount(10)],
};
const isFeeOrdersRequired = utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts);
expect(isFeeOrdersRequired).to.equal(false);
});
});
});

View File

@ -16,7 +16,7 @@
}, },
{ {
"note": "Deploy Forwarder AFTER staking is hooked up", "note": "Deploy Forwarder AFTER staking is hooked up",
"pr": "TODO" "pr": 2350
} }
] ]
}, },