@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",
"changes": [
@ -7,7 +20,7 @@
"pr": 2342
}
],
"timestamp": 1574030254
"timestamp": 1573159180
},
{
"version": "2.1.0-beta.1",

View File

@ -5,10 +5,6 @@ Edit the package's CHANGELOG.json file only.
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_
* 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",
"version": "2.1.0-beta.2",
"version": "2.1.0-beta.1",
"engines": {
"node": ">=6.12"
},
@ -40,28 +40,30 @@
},
"homepage": "https://0x.org/asset-swapper",
"dependencies": {
"@0x/assert": "^2.2.0-beta.2",
"@0x/connect": "^5.1.0-beta.2",
"@0x/contract-addresses": "^3.3.0-beta.3",
"@0x/contract-wrappers": "^12.2.0-beta.2",
"@0x/dev-utils": "^2.4.0-beta.2",
"@0x/json-schemas": "^4.1.0-beta.2",
"@0x/migrations": "^4.4.0-beta.2",
"@0x/order-utils": "^8.5.0-beta.2",
"@0x/orderbook": "^0.1.0-beta.2",
"@0x/subproviders": "^5.1.0-beta.2",
"@0x/types": "^2.5.0-beta.2",
"@0x/typescript-typings": "^4.4.0-beta.2",
"@0x/utils": "^4.6.0-beta.2",
"@0x/web3-wrapper": "^6.1.0-beta.2",
"ethereum-types": "^2.2.0-beta.2",
"@0x/assert": "^2.2.0-beta.1",
"@0x/contracts-dev-utils": "^0.1.0-beta.1",
"@0x/contracts-erc20": "^2.3.0-beta.1",
"@0x/contracts-exchange": "^2.2.0-beta.1",
"@0x/contracts-exchange-forwarder": "^3.1.0-beta.1",
"@0x/json-schemas": "^4.1.0-beta.1",
"@0x/order-utils": "^8.5.0-beta.1",
"@0x/orderbook": "^0.1.0-beta.1",
"@0x/types": "^2.5.0-beta.1",
"@0x/utils": "^4.6.0-beta.1",
"@0x/web3-wrapper": "^6.1.0-beta.1",
"ethereum-types": "^2.2.0-beta.1",
"lodash": "^4.17.11"
},
"devDependencies": {
"@0x/contract-addresses": "^3.3.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/migrations": "^4.4.0-beta.1",
"@0x/subproviders": "^5.1.0-beta.1",
"@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/mocha": "^5.2.7",
"@types/node": "*",

View File

@ -1,55 +1,68 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import {
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
OrdersAndFillableAmounts,
ForwarderExtensionContractOpts,
OrderPrunerOpts,
OrderPrunerPermittedFeeTypes,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
SwapQuoteRequestOpts,
SwapQuoterOpts,
} from './types';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x';
const NULL_ERC20_ASSET_DATA = '0xf47261b00000000000000000000000000000000000000000000000000000000000000000';
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const MAINNET_CHAIN_ID = 1;
const ONE_SECOND_MS = 1000;
const DEFAULT_PER_PAGE = 1000;
const PROTOCOL_FEE_MULTIPLIER = 150000;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
chainId: MAINNET_CHAIN_ID,
orderRefreshIntervalMs: 10000, // 10 seconds
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
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,
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 = {
shouldDisableRequestingFeeOrders: false,
slippagePercentage: 0.2, // 20% slippage protection,
};
const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = {
orders: [] as SignedOrder[],
remainingFillableMakerAssetAmounts: [] as BigNumber[],
};
export const constants = {
ETH_GAS_STATION_API_BASE_URL,
NULL_BYTES,
ZERO_AMOUNT: new BigNumber(0),
NULL_ADDRESS,
MAINNET_CHAIN_ID,
DEFAULT_ORDER_PRUNER_OPTS,
ETHER_TOKEN_DECIMALS: 18,
ONE_AMOUNT: new BigNumber(1),
MAX_AFFILIATE_FEE_PERCENTAGE: 0.05,
ONE_SECOND_MS,
DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
EMPTY_ORDERS_AND_FILLABLE_AMOUNTS,
DEFAULT_PER_PAGE,
PROTOCOL_FEE_MULTIPLIER,
NULL_ERC20_ASSET_DATA,
};

View File

@ -20,11 +20,13 @@ export {
ConstructorStateMutability,
} 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 { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { SwapQuoter } from './swap_quoter';
export { protocolFeeUtils } from './utils/protocol_fee_utils';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { InsufficientAssetLiquidityError } from './errors';
export {
@ -34,21 +36,16 @@ export {
SwapQuoteConsumerOpts,
CalldataInfo,
ExtensionContractType,
SwapQuoteConsumingOpts,
LiquidityForTakerMakerAssetDataPair,
SwapQuoteGetOutputOpts,
PrunedSignedOrder,
SwapQuoteExecutionOpts,
SwapQuoteInfo,
GetExtensionContractTypeOpts,
SwapQuoteExecutionOptsBase,
SwapQuoteGetOutputOptsBase,
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
SmartContractParamsInfo,
MarketBuySwapQuote,
MarketSellSwapQuote,
MarketBuySwapQuoteWithAffiliateFee,
MarketSellSwapQuoteWithAffiliateFee,
LiquidityForAssetData,
OrdersAndFillableAmounts,
SwapQuoteConsumerBase,
SwapQuoteRequestOpts,
} from './types';

View File

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

View File

@ -1,5 +1,6 @@
import { ContractError, ContractWrappers, ForwarderError } from '@0x/contract-wrappers';
import { MarketOperation } from '@0x/types';
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ForwarderContract } from '@0x/contracts-exchange-forwarder';
import { AbiEncoder, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import { MethodAbi } from 'ethereum-types';
@ -8,14 +9,15 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalldataInfo,
ForwarderExtensionContractOpts,
ForwarderSmartContractParams,
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
MarketOperation,
SmartContractParamsInfo,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerError,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
import { assert } from '../utils/assert';
@ -26,18 +28,23 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
public readonly provider: ZeroExProvider;
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);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._contractWrappers = new ContractWrappers(this.provider, {
chainId,
});
this._contractAddresses = contractAddresses;
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(
quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteGetOutputOpts>,
opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
): Promise<CalldataInfo> {
assert.isValidForwarderSwapQuote('quote', quote, await this._getEtherTokenAssetDataOrThrowAsync());
const { toAddress, methodAbi, ethAmount, params } = await this.getSmartContractParamsOrThrowAsync(quote, opts);
const abiEncoder = new AbiEncoder.Method(methodAbi);
const { orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient } = params;
const { orders, signatures, feePercentage, feeRecipient } = params;
let args: any[];
if (params.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = params;
args = [orders, makerAssetFillAmount, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient];
args = [orders, makerAssetFillAmount, signatures, feePercentage, feeRecipient];
} else {
args = [orders, signatures, feeOrders, feeSignatures, feePercentage, feeRecipient];
args = [orders, signatures, feePercentage, feeRecipient];
}
const calldataHexString = abiEncoder.encode(args, { shouldOptimize: true });
return {
@ -79,47 +86,42 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
*/
public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteGetOutputOpts>,
opts: Partial<SwapQuoteGetOutputOpts & ForwarderExtensionContractOpts> = {},
): Promise<SmartContractParamsInfo<ForwarderSmartContractParams>> {
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,
opts,
);
assert.isValidPercentage('feePercentage', unFormattedFeePercentage);
assert.isValidPercentage('feePercentage', feePercentage);
assert.isETHAddressHex('feeRecipient', feeRecipient);
if (ethAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount);
if (providedEthAmount !== undefined) {
assert.isBigNumber('ethAmount', providedEthAmount);
}
const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, unFormattedFeePercentage);
const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee;
const { orders, worstCaseQuoteInfo } = quote;
// lowercase input addresses
const normalizedFeeRecipientAddress = feeRecipient.toLowerCase();
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 methodName: string;
if (quoteWithAffiliateFee.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quoteWithAffiliateFee;
if (quote.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quote;
params = {
orders,
makerAssetFillAmount,
signatures,
feeOrders,
feeSignatures,
feePercentage,
feePercentage: formattedFeePercentage,
feeRecipient: normalizedFeeRecipientAddress,
type: MarketOperation.Buy,
};
@ -129,23 +131,21 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
params = {
orders,
signatures,
feeOrders,
feeSignatures,
feePercentage,
feePercentage: formattedFeePercentage,
feeRecipient: normalizedFeeRecipientAddress,
type: MarketOperation.Sell,
};
methodName = 'marketSellOrdersWithEth';
}
const methodAbi = utils.getMethodAbiFromContractAbi(
this._contractWrappers.forwarder.abi,
methodName,
) as MethodAbi;
const methodAbi = utils.getMethodAbiFromContractAbi(this._forwarder.abi, methodName) as MethodAbi;
const ethAmountWithFees = affiliateFeeUtils
.getTotalEthAmountWithAffiliateFee(worstCaseQuoteInfo, feePercentage)
.plus(worstCaseQuoteInfo.protocolFeeInEthAmount);
return {
params,
toAddress: this._contractWrappers.forwarder.address,
ethAmount: ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount,
toAddress: this._forwarder.address,
ethAmount: providedEthAmount || ethAmountWithFees,
methodAbi,
};
}
@ -157,11 +157,11 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
*/
public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteExecutionOpts>,
opts: Partial<SwapQuoteExecutionOpts & ForwarderExtensionContractOpts>,
): Promise<string> {
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,
opts,
@ -169,8 +169,8 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
assert.isValidPercentage('feePercentage', feePercentage);
assert.isETHAddressHex('feeRecipient', feeRecipient);
if (ethAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount);
if (providedEthAmount !== undefined) {
assert.isBigNumber('ethAmount', providedEthAmount);
}
if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress);
@ -182,21 +182,20 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
assert.isBigNumber('gasPrice', gasPrice);
}
const quoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, feePercentage);
const { orders, feeOrders, worstCaseQuoteInfo } = quoteWithAffiliateFee; // tslint:disable-line:no-unused-variable
const { orders, worstCaseQuoteInfo } = quote; // tslint:disable-line:no-unused-variable
// get taker address
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
// if no ethAmount is provided, default to the worst totalTakerTokenAmount
const value = ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount;
// if no ethAmount is provided, default to the worst totalTakerAssetAmount
const ethAmountWithFees = affiliateFeeUtils
.getTotalEthAmountWithAffiliateFee(worstCaseQuoteInfo, feePercentage)
.plus(worstCaseQuoteInfo.protocolFeeInEthAmount);
// format fee percentage
const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage);
try {
let txHash: string;
if (quoteWithAffiliateFee.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quoteWithAffiliateFee;
txHash = await this._contractWrappers.forwarder
if (quote.type === MarketOperation.Buy) {
const { makerAssetFillAmount } = quote;
txHash = await this._forwarder
.marketBuyOrdersWithEth(
orders,
makerAssetFillAmount,
@ -205,36 +204,28 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase<Forward
feeRecipient,
)
.sendTransactionAsync({
value,
from: finalTakerAddress.toLowerCase(),
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value: providedEthAmount || ethAmountWithFees,
});
} else {
txHash = await this._contractWrappers.forwarder
txHash = await this._forwarder
.marketSellOrdersWithEth(orders, orders.map(o => o.signature), formattedFeePercentage, feeRecipient)
.sendTransactionAsync({
value,
from: finalTakerAddress.toLowerCase(),
from: finalTakerAddress,
gas: gasLimit,
gasPrice,
value: providedEthAmount || ethAmountWithFees,
});
}
// TODO(dorothy-zbornak): Handle signature request denied
// (see contract-wrappers/decorators)
// and ForwarderRevertErrors.CompleteBuyFailed.
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;
}
}
}
private async _getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._contractWrappers.devUtils
.encodeERC20AssetData(this._contractWrappers.contractAddresses.etherToken)
.callAsync();
return this._devUtils.encodeERC20AssetData(this._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 { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
@ -13,6 +13,7 @@ import {
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
SwapQuoteConsumingOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
@ -28,7 +29,14 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
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> = {}) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
@ -37,22 +45,19 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase<SmartContractPar
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, options);
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, options);
this._contractWrappers = new ContractWrappers(this.provider, {
chainId,
});
this._contractAddresses = getContractAddressesForChainOrThrow(chainId);
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
}
/**
* 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 opts Options for getting SmartContractParams. See type definition for more information.
*/
public async getCalldataOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
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 opts Options for getting SmartContractParams. See type definition for more information.
*/
public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
opts: Partial<SwapQuoteGetOutputOpts & SwapQuoteConsumingOpts> = {},
): Promise<SmartContractParamsInfo<SmartContractParams>> {
assert.isValidSwapQuote('quote', quote);
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 opts Options for getting CalldataInfo. See type definition for more information.
*/
public async executeSwapQuoteOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts> = {},
opts: Partial<SwapQuoteExecutionOpts & SwapQuoteConsumingOpts> = {},
): Promise<string> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(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(
quote: SwapQuote,
opts: Partial<GetExtensionContractTypeOpts> = {},
): Promise<ExtensionContractType> {
return swapQuoteConsumerUtils.getExtensionContractTypeForSwapQuoteAsync(
quote,
this._contractWrappers,
this._contractAddresses,
this.provider,
opts,
);
}
private async _getConsumerForSwapQuoteAsync(
opts: Partial<SwapQuoteGetOutputOpts>,
opts: Partial<SwapQuoteConsumingOpts>,
): Promise<SwapQuoteConsumerBase<SmartContractParams>> {
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
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 { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { SignedOrder } from '@0x/order-utils';
import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { MarketOperation } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from './constants';
import {
LiquidityForAssetData,
LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
OrdersAndFillableAmounts,
OrderPrunerPermittedFeeTypes,
PrunedSignedOrder,
SwapQuote,
SwapQuoteRequestOpts,
SwapQuoterError,
@ -20,16 +22,19 @@ import {
} from './types';
import { assert } from './utils/assert';
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 { utils } from './utils/utils';
export class SwapQuoter {
public readonly provider: ZeroExProvider;
public readonly orderbook: Orderbook;
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.
* @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
*/
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);
assert.isValidOrderbook('orderbook', orderbook);
assert.isNumber('chainId', chainId);
@ -140,10 +149,15 @@ export class SwapQuoter {
this.provider = provider;
this.orderbook = orderbook;
this.expiryBufferMs = expiryBufferMs;
this._contractWrappers = new ContractWrappers(this.provider, {
chainId,
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
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.
* 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('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount);
const makerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(makerTokenAddress)
.callAsync();
const takerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(takerTokenAddress)
.callAsync();
const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
const swapQuote = this.getMarketBuySwapQuoteForAssetDataAsync(
makerAssetData,
takerAssetData,
@ -248,12 +258,8 @@ export class SwapQuoter {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount);
const makerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(makerTokenAddress)
.callAsync();
const takerAssetData = await this._contractWrappers.devUtils
.encodeERC20AssetData(takerTokenAddress)
.callAsync();
const makerAssetData = await this._devUtilsContract.encodeERC20AssetData(makerTokenAddress).callAsync();
const takerAssetData = await this._devUtilsContract.encodeERC20AssetData(takerTokenAddress).callAsync();
const swapQuote = this.getMarketSellSwapQuoteForAssetDataAsync(
makerAssetData,
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 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(
makerAssetData: string,
takerAssetData: string,
): Promise<LiquidityForAssetData> {
): Promise<LiquidityForTakerMakerAssetDataPair> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
const assetPairs = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
if (!assetPairs.includes(makerAssetData)) {
return {
makerTokensAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0),
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
};
}
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData);
return calculateLiquidity(ordersAndFillableAmounts);
const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
return calculateLiquidity(prunedOrders);
}
/**
@ -299,7 +304,7 @@ export class SwapQuoter {
*/
public async getAvailableTakerAssetDatasAsync(makerAssetData: string): Promise<string[]> {
assert.isString('makerAssetData', makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.filter(pair => pair.assetDataA.assetData === makerAssetData)
@ -314,7 +319,7 @@ export class SwapQuoter {
*/
public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> {
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
const allAssetPairs = await this.orderbook.getAvailableAssetDatasAsync();
const assetPairs = allAssetPairs
.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`.
* @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
*/
@ -333,41 +340,31 @@ export class SwapQuoter {
): Promise<boolean> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
const availableMakerAssetDatas = await this.getAvailableMakerAssetDatasAsync(takerAssetData);
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 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,
takerAssetData: string,
): Promise<OrdersAndFillableAmounts> {
): Promise<PrunedSignedOrder[]> {
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
assetDataUtils.decodeAssetDataOrThrow(takerAssetData);
const zrxTokenAssetData = await this._getZrxTokenAssetDataOrThrowAsync();
await this._devUtilsContract.revertIfInvalidAssetData(takerAssetData).callAsync();
await this._devUtilsContract.revertIfInvalidAssetData(makerAssetData).callAsync();
// get orders
const response = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const adaptedResponse = { orders: response.map(o => ({ ...o.order, ...o.metaData })) };
// since the order provider is an injected dependency, validate that it respects the API
// ie. it should only return maker/taker assetDatas that are specified
orderProviderResponseProcessor.throwIfInvalidResponse(adaptedResponse, makerAssetData, takerAssetData);
// process the responses into one object
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
const ordersAndFillableAmounts = await orderProviderResponseProcessor.processAsync(
adaptedResponse,
isMakerAssetZrxToken,
this.expiryBufferMs,
this._contractWrappers.orderValidator,
);
return ordersAndFillableAmounts;
const apiOrders = await this.orderbook.getOrdersAsync(makerAssetData, takerAssetData);
const orders = _.map(apiOrders, o => o.order);
const prunedOrders = await this._orderPruner.pruneSignedOrdersAsync(orders);
const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders);
return sortedPrunedOrders;
}
/**
@ -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 takerAddress The address of the taker of the provided swapQuote
*/
public async isTakerAddressAllowanceEnoughForBestAndWorstQuoteInfoAsync(
public async isSwapQuoteFillableByTakerAddressAsync(
swapQuote: SwapQuote,
takerAddress: string,
): Promise<[boolean, boolean]> {
const orderValidator = this._contractWrappers.orderValidator;
const balanceAndAllowance = await orderValidator
.getBalanceAndAllowance(takerAddress, swapQuote.takerAssetData)
const balanceAndAllowance = await this._devUtilsContract
.getBalanceAndAssetProxyAllowance(takerAddress, swapQuote.takerAssetData)
.callAsync();
const allowance = balanceAndAllowance[1];
return [
allowance.isGreaterThanOrEqualTo(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount),
allowance.isGreaterThanOrEqualTo(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount),
balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.bestCaseQuoteInfo.totalTakerAssetAmount),
balanceAndAllowance[1].isGreaterThanOrEqualTo(swapQuote.worstCaseQuoteInfo.totalTakerAssetAmount),
];
}
@ -398,13 +393,10 @@ export class SwapQuoter {
}
/**
* Get the assetData that represents the ZRX token.
* Will throw if ZRX does not exist for the current chain.
* Utility function to get assetData for Ether token.
*/
private async _getZrxTokenAssetDataOrThrowAsync(): Promise<string> {
return this._contractWrappers.devUtils
.encodeERC20AssetData(this._contractWrappers.contractAddresses.zrxToken)
.callAsync();
public async getEtherTokenAssetDataOrThrowAsync(): Promise<string> {
return this._devUtilsContract.encodeERC20AssetData(this._contractAddresses.etherToken).callAsync();
}
/**
@ -417,30 +409,20 @@ export class SwapQuoter {
marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> {
const { slippagePercentage, shouldDisableRequestingFeeOrders } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
const { slippagePercentage } = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isNumber('slippagePercentage', slippagePercentage);
const zrxTokenAssetData = await this._getZrxTokenAssetDataOrThrowAsync();
const isMakerAssetZrxToken = makerAssetData === zrxTokenAssetData;
// get the relevant orders for the makerAsset
const ordersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(makerAssetData, takerAssetData);
const doesOrdersRequireFeeOrders =
!isMakerAssetZrxToken && utils.isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts);
const isRequestingFeeOrders = !shouldDisableRequestingFeeOrders && doesOrdersRequireFeeOrders;
let feeOrdersAndFillableAmounts = constants.EMPTY_ORDERS_AND_FILLABLE_AMOUNTS;
if (isRequestingFeeOrders) {
feeOrdersAndFillableAmounts = await this.getOrdersAndFillableAmountsAsync(
zrxTokenAssetData,
takerAssetData,
);
let gasPrice: BigNumber;
if (!!options.gasPrice) {
gasPrice = options.gasPrice;
assert.isBigNumber('gasPrice', gasPrice);
} else {
gasPrice = await protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
}
if (ordersAndFillableAmounts.orders.length === 0) {
// get the relevant orders for the makerAsset
const prunedOrders = await this.getPrunedSignedOrdersAsync(makerAssetData, takerAssetData);
if (prunedOrders.length === 0) {
throw new Error(
`${
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;
if (marketOperation === MarketOperation.Buy) {
swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
prunedOrders,
assetFillAmount,
slippagePercentage,
isMakerAssetZrxToken,
shouldDisableRequestingFeeOrders,
gasPrice,
);
} else {
swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
prunedOrders,
assetFillAmount,
slippagePercentage,
isMakerAssetZrxToken,
shouldDisableRequestingFeeOrders,
gasPrice,
);
}

View File

@ -1,7 +1,27 @@
import { MarketOperation, SignedOrder } from '@0x/types';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
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.
* 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 {
orders: SignedOrderWithRemainingFillableMakerAssetAmount[];
}
/**
* 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;
export interface PrunedSignedOrder extends SignedOrder {
fillableMakerAssetAmount: BigNumber;
fillableTakerAssetAmount: BigNumber;
fillableTakerFeeAmount: BigNumber;
}
/**
@ -31,13 +47,13 @@ export interface SignedOrderWithRemainingFillableMakerAssetAmount extends Signed
* calldataHexString: The hexstring of the calldata.
* methodAbi: The ABI of the smart contract method 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 {
calldataHexString: string;
methodAbi: MethodAbi;
toAddress: string;
ethAmount?: BigNumber;
ethAmount: BigNumber;
}
/**
@ -50,7 +66,7 @@ export interface CalldataInfo {
export interface SmartContractParamsInfo<T> {
params: T;
toAddress: string;
ethAmount?: BigNumber;
ethAmount: BigNumber;
methodAbi: MethodAbi;
}
@ -95,14 +111,10 @@ export enum ExtensionContractType {
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.
* feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
*/
export interface ForwarderSmartContractParamsBase {
feeOrders: SignedOrder[];
feeSignatures: string[];
feePercentage: BigNumber;
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.
*/
export interface SwapQuoteConsumerBase<T> {
getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteGetOutputOptsBase>): Promise<CalldataInfo>;
getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteGetOutputOpts>): Promise<CalldataInfo>;
getSmartContractParamsOrThrowAsync(
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOptsBase>,
opts: Partial<SwapQuoteGetOutputOpts>,
): 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
*/
export interface SwapQuoteGetOutputOptsBase {}
export interface SwapQuoteGetOutputOpts {}
/**
* 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.
* 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;
gasLimit?: number;
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
* 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;
feeRecipient: string;
ethAmount?: BigNumber;
}
/*
* Options for how SwapQuoteConsumer will generate output
*/
export interface SwapQuoteConsumingOpts {
useExtensionContract: ExtensionContractType;
}
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
@ -186,26 +207,10 @@ export interface GetExtensionContractTypeOpts {
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).
* 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.
* 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.
* worstCaseQuoteInfo: Info about the worst case price for the asset.
*/
@ -213,7 +218,6 @@ export interface SwapQuoteBase {
takerAssetData: string;
makerAssetData: string;
orders: SignedOrder[];
feeOrders: SignedOrder[];
bestCaseQuoteInfo: SwapQuoteInfo;
worstCaseQuoteInfo: SwapQuoteInfo;
}
@ -236,36 +240,28 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
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.
* takerTokenAmount: The amount of takerToken required to conduct the swap.
* totalTakerTokenAmount: The total amount of takerToken required to complete the swap (filling orders, feeOrders, and paying affiliate fee)
* makerTokenAmount: The amount of makerToken that will be acquired through the swap.
* feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerAssetAmount: The amount of takerAsset swapped for desired makerAsset.
* totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* 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 {
feeTakerTokenAmount: BigNumber;
totalTakerTokenAmount: BigNumber;
takerTokenAmount: BigNumber;
makerTokenAmount: BigNumber;
feeTakerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
totalTakerAssetAmount: 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%).
* gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount
*/
export interface SwapQuoteRequestOpts {
shouldDisableRequestingFeeOrders: boolean;
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).
* 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;
orderRefreshIntervalMs: number;
expiryBufferMs: number;
@ -295,28 +291,33 @@ export enum SwapQuoteConsumerError {
*/
export enum SwapQuoterError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR',
InsufficientAssetLiquidity = 'INSUFFICIENT_ASSET_LIQUIDITY',
InsufficientZrxLiquidity = 'INSUFFICIENT_ZRX_LIQUIDITY',
InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE',
AssetUnavailable = 'ASSET_UNAVAILABLE',
FeeAssetUnavailable = 'FEE_ASSET_UNAVAILABLE',
NoGasPriceProvidedOrEstimated = 'NO_GAS_PRICE_PROVIDED_OR_ESTIMATED',
}
/**
* orders: An array of signed orders
* 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.
* Represents available liquidity for a given assetData.
*/
export interface OrdersAndFillableAmounts {
orders: SignedOrder[];
remainingFillableMakerAssetAmounts: BigNumber[];
export interface LiquidityForTakerMakerAssetDataPair {
makerAssetAvailableInBaseUnits: BigNumber;
takerAssetAvailableInBaseUnits: BigNumber;
}
/**
* Represents available liquidity for a given assetData
* Represents two main market operations supported by asset-swapper.
*/
export interface LiquidityForAssetData {
makerTokensAvailableInBaseUnits: BigNumber;
takerTokensAvailableInBaseUnits: BigNumber;
export enum MarketOperation {
Sell = 'Sell',
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 * as _ from 'lodash';
import { SwapQuote, SwapQuoteInfo, SwapQuoteWithAffiliateFee } from '../types';
import { constants } from '../constants';
import { SwapQuoteInfo } from '../types';
import { assert } from './assert';
export const affiliateFeeUtils = {
getSwapQuoteWithAffiliateFee(quote: SwapQuote, feePercentage: number): SwapQuoteWithAffiliateFee {
const newQuote = _.clone(quote);
newQuote.bestCaseQuoteInfo = getSwapQuoteInfoWithAffiliateFee(newQuote.bestCaseQuoteInfo, feePercentage);
newQuote.worstCaseQuoteInfo = getSwapQuoteInfoWithAffiliateFee(newQuote.worstCaseQuoteInfo, feePercentage);
return { ...newQuote, ...{ feePercentage } };
/**
* Get the amount of eth to send for a forwarder contract call (includes takerAssetAmount, protocol fees, and specified affiliate fee amount)
* @param swapQuoteInfo SwapQuoteInfo to generate total eth amount from
* @param feePercentage Percentage of additive fees to apply to totalTakerAssetAmount + protocol fee. (max 5%)
*/
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 { schemas } from '@0x/json-schemas';
import { Orderbook } from '@0x/orderbook';
import { MarketOperation, SignedOrder } from '@0x/types';
import { Order, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { utils } from './utils';
export const assert = {
...sharedAssert,
@ -12,8 +14,7 @@ export const assert = {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema);
assert.isValidOrdersForSwapQuote(
assert.isValidSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
@ -27,7 +28,7 @@ export const assert = {
sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount);
}
},
isValidOrdersForSwapQuote(
isValidSwapQuoteOrders(
variableName: string,
orders: SignedOrder[],
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 {
assert.isValidSwapQuote(variableName, swapQuote);
assert.isValidForwarderSignedOrders(`${variableName}.orders`, swapQuote.orders, wethAssetData);
assert.isValidForwarderSignedOrders(`${variableName}.feeOrders`, swapQuote.feeOrders, wethAssetData);
},
isValidForwarderSignedOrders(variableName: string, orders: SignedOrder[], wethAssetData: string): void {
_.forEach(orders, (o: SignedOrder, i: number) => {
@ -65,10 +77,10 @@ export const assert = {
);
},
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, swapQuoteInfo.feeTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, swapQuoteInfo.totalTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.makerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.makerAssetAmount`, swapQuoteInfo.makerAssetAmount);
},
isValidOrderbook(variableName: string, orderFetcher: Orderbook): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);

View File

@ -1,40 +1,27 @@
import { orderCalculationUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../types';
import { LiquidityForTakerMakerAssetDataPair, PrunedSignedOrder } from '../types';
export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAmounts): LiquidityForAssetData => {
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}`);
}
import { utils } from './utils';
const makerTokensAvailableForCurrentOrder = availableMakerAssetAmount;
const takerTokensAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount(
order,
makerTokensAvailableForCurrentOrder,
);
export const calculateLiquidity = (prunedOrders: PrunedSignedOrder[]): LiquidityForTakerMakerAssetDataPair => {
const liquidityInBigNumbers = prunedOrders.reduce(
(acc, order) => {
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 {
makerTokensAvailableInBaseUnits: acc.makerTokensAvailableInBaseUnits.plus(
makerTokensAvailableForCurrentOrder,
),
takerTokensAvailableInBaseUnits: acc.takerTokensAvailableInBaseUnits.plus(
takerTokensAvailableForCurrentOrder,
),
makerAssetAvailableInBaseUnits: acc.makerAssetAvailableInBaseUnits.plus(fillableMakerAssetAmount),
takerAssetAvailableInBaseUnits: acc.takerAssetAvailableInBaseUnits.plus(fillableTakerAssetAmount),
};
},
{
makerTokensAvailableInBaseUnits: new BigNumber(0),
takerTokensAvailableInBaseUnits: new BigNumber(0),
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
},
);
// Turn into regular numbers
return {
makerTokensAvailableInBaseUnits: liquidityInBigNumbers.makerTokensAvailableInBaseUnits,
takerTokensAvailableInBaseUnits: liquidityInBigNumbers.takerTokensAvailableInBaseUnits,
};
return liquidityInBigNumbers;
};

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 { MarketOperation } from '@0x/types';
import { orderCalculationUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
@ -7,106 +6,75 @@ import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors';
import {
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
OrdersAndFillableAmounts,
PrunedSignedOrder,
SwapQuote,
SwapQuoteInfo,
SwapQuoterError,
} from '../types';
import { marketUtils } from './market_utils';
import { protocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils';
// Calculates a swap quote for orders
export const swapQuoteCalculator = {
calculateMarketSellSwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
prunedOrders: PrunedSignedOrder[],
takerAssetFillAmount: BigNumber,
slippagePercentage: number,
isMakerAssetZrxToken: boolean,
shouldDisableFeeOrderCalculations: boolean,
gasPrice: BigNumber,
): MarketSellSwapQuote {
return calculateSwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
prunedOrders,
takerAssetFillAmount,
slippagePercentage,
isMakerAssetZrxToken,
shouldDisableFeeOrderCalculations,
gasPrice,
MarketOperation.Sell,
) as MarketSellSwapQuote;
},
calculateMarketBuySwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
makerAssetFillAmount: BigNumber,
prunedOrders: PrunedSignedOrder[],
takerAssetFillAmount: BigNumber,
slippagePercentage: number,
isMakerAssetZrxToken: boolean,
shouldDisableFeeOrderCalculations: boolean,
gasPrice: BigNumber,
): MarketBuySwapQuote {
return calculateSwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
makerAssetFillAmount,
prunedOrders,
takerAssetFillAmount,
slippagePercentage,
isMakerAssetZrxToken,
shouldDisableFeeOrderCalculations,
gasPrice,
MarketOperation.Buy,
) as MarketBuySwapQuote;
},
};
function calculateSwapQuote(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
prunedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
slippagePercentage: number,
isMakerAssetZrxToken: boolean,
shouldDisableFeeOrderCalculations: boolean,
gasPrice: BigNumber,
marketOperation: MarketOperation,
): 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();
let resultOrders: SignedOrder[];
let resultOrders: PrunedSignedOrder[];
let remainingFillAmount: BigNumber;
let ordersRemainingFillableMakerAssetAmounts: BigNumber[];
if (marketOperation === MarketOperation.Buy) {
// find the orders that cover the desired assetBuyAmount (with slippage)
({
resultOrders,
remainingFillAmount,
ordersRemainingFillableMakerAssetAmounts,
} = marketUtils.findOrdersThatCoverMakerAssetFillAmount(orders, assetFillAmount, {
remainingFillableMakerAssetAmounts,
({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
prunedOrders,
assetFillAmount,
slippageBufferAmount,
}));
));
} else {
let ordersRemainingFillableTakerAssetAmounts: BigNumber[];
// find the orders that cover the desired assetBuyAmount (with slippage)
({
resultOrders,
remainingFillAmount,
ordersRemainingFillableTakerAssetAmounts,
} = marketUtils.findOrdersThatCoverTakerAssetFillAmount(orders, assetFillAmount, {
remainingFillableTakerAssetAmounts,
({ resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount(
prunedOrders,
assetFillAmount,
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
@ -126,60 +94,16 @@ function calculateSwapQuote(
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
const takerAssetData = orders[0].takerAssetData;
const makerAssetData = orders[0].makerAssetData;
const takerAssetData = resultOrders[0].takerAssetData;
const makerAssetData = resultOrders[0].makerAssetData;
// compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultOrders,
remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
};
const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultFeeOrders,
remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
};
const bestCaseQuoteInfo = calculateQuoteInfo(
trimmedOrdersAndFillableAmounts,
trimmedFeeOrdersAndFillableAmounts,
assetFillAmount,
isMakerAssetZrxToken,
shouldDisableFeeOrderCalculations,
marketOperation,
);
const bestCaseQuoteInfo = calculateQuoteInfo(resultOrders, assetFillAmount, gasPrice, marketOperation);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = calculateQuoteInfo(
reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts),
reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
_.reverse(_.clone(resultOrders)),
assetFillAmount,
isMakerAssetZrxToken,
shouldDisableFeeOrderCalculations,
gasPrice,
marketOperation,
);
@ -187,7 +111,6 @@ function calculateSwapQuote(
takerAssetData,
makerAssetData,
orders: resultOrders,
feeOrders: resultFeeOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
};
@ -208,199 +131,159 @@ function calculateSwapQuote(
}
function calculateQuoteInfo(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
tokenAmount: BigNumber,
isMakerAssetZrxToken: boolean,
shouldDisableFeeOrderCalculations: boolean,
marketOperation: MarketOperation,
prunedOrders: PrunedSignedOrder[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
operation: MarketOperation,
): SwapQuoteInfo {
// find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right
let makerTokenAmount = marketOperation === MarketOperation.Buy ? tokenAmount : constants.ZERO_AMOUNT;
let takerTokenAmount = marketOperation === MarketOperation.Sell ? tokenAmount : constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
if (isMakerAssetZrxToken) {
if (marketOperation === MarketOperation.Buy) {
takerTokenAmount = findTakerTokenAmountNeededToBuyZrx(ordersAndFillableAmounts, makerTokenAmount);
if (operation === MarketOperation.Buy) {
return calculateMarketBuyQuoteInfo(prunedOrders, assetFillAmount, gasPrice);
} else {
makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount(
ordersAndFillableAmounts,
takerTokenAmount,
);
return calculateMarketSellQuoteInfo(prunedOrders, assetFillAmount, gasPrice);
}
} else {
const findTokenAndZrxAmount =
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(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
function calculateMarketSellQuoteInfo(
prunedOrders: PrunedSignedOrder[],
takerAssetSellAmount: BigNumber,
): BigNumber {
const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
gasPrice: BigNumber,
): SwapQuoteInfo {
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalZrxTokenAmount, remainingTakerAssetFillAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount(
order,
remainingFillableMakerAssetAmount,
prunedOrders,
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
remainingTakerAssetFillAmount,
} = acc;
const [
adjustedFillableMakerAssetAmount,
adjustedFillableTakerAssetAmount,
] = utils.getAdjustedFillableMakerAndTakerAmountsFromTakerFees(order);
const takerAssetAmountWithFees = BigNumber.min(
remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount,
);
const takerFillAmount = BigNumber.min(remainingTakerAssetFillAmount, remainingFillableTakerAssetAmount);
const makerFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerFillAmount);
const feeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown(
order,
takerAssetAmountWithFees,
);
const makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return {
totalZrxTokenAmount: totalZrxTokenAmount.plus(makerFillAmount).minus(feeAmount),
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingTakerAssetFillAmount: BigNumber.max(
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,
},
);
return result.totalZrxTokenAmount;
}
function findTakerTokenAmountNeededToBuyZrx(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
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),
),
feeTakerAssetAmount: result.totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount,
protocolFeeInEthAmount: protocolFeeUtils.calculateWorstCaseProtocolFee(prunedOrders, gasPrice),
};
},
{
totalTakerTokenAmount: constants.ZERO_AMOUNT,
remainingZrxBuyAmount: zrxBuyAmount,
},
);
return result.totalTakerTokenAmount;
}
function findTakerTokenAndZrxAmountNeededToBuyAsset(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
function calculateMarketBuyQuoteInfo(
prunedOrders: PrunedSignedOrder[],
makerAssetBuyAmount: BigNumber,
): [BigNumber, BigNumber] {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
gasPrice: BigNumber,
): SwapQuoteInfo {
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalTakerTokenAmount, totalZrxAmount, remainingmakerAssetFillAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(acc.remainingmakerAssetFillAmount, remainingFillableMakerAssetAmount);
const takerFillAmount = orderCalculationUtils.getTakerFillAmount(order, makerFillAmount);
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
prunedOrders,
(acc, order) => {
const {
totalMakerAssetAmount,
totalTakerAssetAmount,
totalFeeTakerAssetAmount,
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 {
totalTakerTokenAmount: totalTakerTokenAmount.plus(takerFillAmount),
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount),
remainingmakerAssetFillAmount: BigNumber.max(
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount),
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount),
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount),
remainingMakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingmakerAssetFillAmount.minus(makerFillAmount),
remainingMakerAssetFillAmount.minus(makerFillAmount),
),
};
},
{
totalTakerTokenAmount: constants.ZERO_AMOUNT,
totalZrxAmount: constants.ZERO_AMOUNT,
remainingmakerAssetFillAmount: makerAssetBuyAmount,
totalMakerAssetAmount: constants.ZERO_AMOUNT,
totalTakerAssetAmount: constants.ZERO_AMOUNT,
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(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
takerAssetSellAmount: BigNumber,
): [BigNumber, BigNumber] {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const result = _.reduce(
orders,
(acc, order, index) => {
const { totalMakerTokenAmount, totalZrxAmount, remainingTakerAssetFillAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount(
order,
remainingFillableMakerAssetAmount,
);
const takerFillAmount = BigNumber.min(acc.remainingTakerAssetFillAmount, remainingFillableTakerAssetAmount);
const makerFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerFillAmount);
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
function getTakerAssetAmountBreakDown(
order: PrunedSignedOrder,
takerAssetAmountWithFees: BigNumber,
): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee);
const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount);
const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL);
return {
totalMakerTokenAmount: totalMakerTokenAmount.plus(makerFillAmount),
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount),
remainingTakerAssetFillAmount: BigNumber.max(
constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount.minus(takerFillAmount),
),
takerAssetAmount,
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
};
} else if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
if (takerAssetAmountWithFees.isZero()) {
return {
takerAssetAmount: constants.ZERO_AMOUNT,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
};
}
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerAssetAmountWithFees);
const makerAssetFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerAssetAmountWithFees);
const takerAssetAmount = takerFeeAmount
.div(makerAssetFillAmount)
.multipliedBy(takerAssetAmountWithFees)
.integerValue(BigNumber.ROUND_CEIL);
return {
takerAssetAmount,
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
};
}
return {
feeTakerAssetAmount: constants.ZERO_AMOUNT,
takerAssetAmount: takerAssetAmountWithFees,
};
},
{
totalMakerTokenAmount: constants.ZERO_AMOUNT,
totalZrxAmount: constants.ZERO_AMOUNT,
remainingTakerAssetFillAmount: takerAssetSellAmount,
},
);
return [result.totalMakerTokenAmount, result.totalZrxAmount];
}

View File

@ -1,5 +1,7 @@
import { ContractWrappers } from '@0x/contract-wrappers';
import { MarketOperation, SignedOrder } from '@0x/types';
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { WETH9Contract } from '@0x/contracts-erc20';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
@ -47,19 +49,17 @@ export const swapQuoteConsumerUtils = {
},
async getEthAndWethBalanceAsync(
provider: SupportedProvider,
contractWrappers: ContractWrappers,
contractAddresses: ContractAddresses,
takerAddress: string,
): Promise<[BigNumber, BigNumber]> {
const weth = new WETH9Contract(contractAddresses.etherToken, provider);
const web3Wrapper = new Web3Wrapper(provider);
const ethBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
const wethBalance = await contractWrappers.weth9.balanceOf(takerAddress).callAsync();
const wethBalance = await weth.balanceOf(takerAddress).callAsync();
return [ethBalance, wethBalance];
},
isValidForwarderSwapQuote(swapQuote: SwapQuote, wethAssetData: string): boolean {
return (
swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData) &&
swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.feeOrders, wethAssetData)
);
return swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData);
},
isValidForwarderSignedOrders(orders: SignedOrder[], wethAssetData: string): boolean {
return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData));
@ -67,35 +67,25 @@ export const swapQuoteConsumerUtils = {
isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean {
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(
quote: SwapQuote,
contractWrappers: ContractWrappers,
contractAddresses: ContractAddresses,
provider: Provider,
opts: Partial<GetExtensionContractTypeOpts>,
): Promise<ExtensionContractType> {
const wethAssetData = await contractWrappers.devUtils
.encodeERC20AssetData(contractWrappers.contractAddresses.etherToken)
.callAsync();
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
const wethAssetData = await devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync();
if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) {
if (opts.takerAddress !== undefined) {
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 takerEthAndWethBalance =
takerAddress !== undefined
? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractWrappers, takerAddress)
? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractAddresses, takerAddress)
: [constants.ZERO_AMOUNT, constants.ZERO_AMOUNT];
// TODO(david): when considering if there is enough Eth balance, should account for gas costs.
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 { Web3Wrapper } from '@0x/web3-wrapper';
import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { OrdersAndFillableAmounts } from '../types';
import { PrunedSignedOrder } from '../types';
// tslint:disable:no-unnecessary-type-assertion
export const utils = {
@ -27,15 +27,30 @@ export const utils = {
},
) as MethodAbi | undefined;
},
isFeeOrdersRequiredToFillOrders(ordersAndFillableAmounts: OrdersAndFillableAmounts): boolean {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
return _.some(
orders,
(order: SignedOrder, index: number): boolean => {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
// If takerFee is a non zero value and order is still fillable, fee orders are required
return !order.takerFee.isZero() && !remainingFillableMakerAssetAmount.isZero();
isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
return order.takerFeeAssetData === order.makerAssetData;
},
);
isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
return order.takerFeeAssetData === order.takerAssetData;
},
getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
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 { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
@ -13,7 +15,9 @@ import {
ExchangeMarketBuySmartContractParams,
ExchangeMarketSellSmartContractParams,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
PrunedSignedOrder,
} from '../src/types';
import { chaiSetup } from './utils/chai_setup';
@ -25,16 +29,47 @@ chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
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 TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const UNLIMITED_ALLOWANCE = 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(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', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[];
let erc20TokenContract: ERC20TokenContract;
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
@ -46,32 +81,38 @@ describe('ExchangeSwapQuoteConsumer', () => {
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let exchangeContract: ExchangeContract;
const chainId = TESTRPC_CHAIN_ID;
let orders: SignedOrder[];
let orders: PrunedSignedOrder[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ExchangeSwapQuoteConsumer;
let expectMakerAndTakerBalancesForMakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
let expectMakerAndTakerBalancesForTakerAssetAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
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();
[makerAssetData, takerAssetData, wethAssetData] = [
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
];
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
[makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
]);
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
@ -79,17 +120,26 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAddress,
makerAssetData,
takerAssetData,
makerFeeAssetData: await contractWrappers.devUtils
.encodeERC20AssetData(contractAddresses.zrxToken)
.callAsync(),
takerFeeAssetData: await contractWrappers.devUtils
.encodeERC20AssetData(contractAddresses.zrxToken)
.callAsync(),
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);
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20TakerTokenContract,
makerAddress,
takerAddress,
);
expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
erc20MakerTokenContract,
makerAddress,
takerAddress,
);
});
after(async () => {
await blockchainLifecycle.revertAsync();
@ -97,12 +147,13 @@ describe('ExchangeSwapQuoteConsumer', () => {
beforeEach(async () => {
await blockchainLifecycle.startAsync();
orders = [];
for (const fillableAmount of FILLABLE_AMOUNTS) {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: fillableAmount,
takerAssetAmount: fillableAmount,
});
orders.push(order);
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
orders.push(prunedOrder as PrunedSignedOrder);
}
marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -110,6 +161,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAssetData,
orders,
MarketOperation.Sell,
GAS_PRICE,
);
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -117,45 +169,87 @@ describe('ExchangeSwapQuoteConsumer', () => {
takerAssetData,
orders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, {
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
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 () => {
await blockchainLifecycle.revertAsync();
});
describe('executeSwapQuoteOrThrowAsync', () => {
describe('#executeSwapQuoteOrThrowAsync', () => {
/*
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await 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 });
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);
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
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 () => {
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);
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await expectMakerAndTakerBalancesForTakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
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 () => {
// TODO(david) Check for valid MethodAbi
it('provide correct and optimized smart contract params for a marketSell SwapQuote', async () => {
@ -163,7 +257,7 @@ describe('ExchangeSwapQuoteConsumer', () => {
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
expect(toAddress).to.deep.equal(exchangeContract.address);
const { takerAssetFillAmount, signatures, type } = params as ExchangeMarketSellSmartContractParams;
expect(type).to.deep.equal(MarketOperation.Sell);
expect(takerAssetFillAmount).to.bignumber.equal(
@ -172,12 +266,12 @@ describe('ExchangeSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
});
it('provide correct and optimized smart contract params for a marketBuy SwapQuote', async () => {
it('provide correct smart contract params for a marketBuy SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
expect(toAddress).to.deep.equal(exchangeContract.address);
const { makerAssetFillAmount, signatures, type } = params as ExchangeMarketBuySmartContractParams;
expect(type).to.deep.equal(MarketOperation.Buy);
expect(makerAssetFillAmount).to.bignumber.equal(
@ -189,49 +283,53 @@ describe('ExchangeSwapQuoteConsumer', () => {
});
});
describe('getCalldataOrThrow', () => {
describe('#getCalldataOrThrow', () => {
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await 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);
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
});
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);
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await 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);
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(
await expectMakerAndTakerBalancesForMakerAssetAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
expect(toAddress).to.deep.equal(exchangeContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
gas: 4000000,
gasPrice: GAS_PRICE,
value: ethAmount,
});
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);
await expectMakerAndTakerBalancesForMakerAssetAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});

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 { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
@ -12,28 +15,61 @@ import {
ForwarderMarketBuySmartContractParams,
ForwarderMarketSellSmartContractParams,
MarketBuySwapQuote,
MarketOperation,
PrunedSignedOrder,
} from '../src/types';
import { chaiSetup } from './utils/chai_setup';
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';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337;
const MARKET_OPERATION = MarketOperation.Sell;
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), 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
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', () => {
let contractWrappers: ContractWrappers;
let erc20Token: ERC20TokenContract;
const FEE_PERCENTAGE = 0.05;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
@ -43,33 +79,68 @@ describe('ForwarderSwapQuoteConsumer', () => {
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let orderFactory: OrderFactory;
let invalidOrderFactory: OrderFactory;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let erc20TokenContract: ERC20TokenContract;
let forwarderContract: ForwarderContract;
let orders: SignedOrder[];
let orders: PrunedSignedOrder[];
let invalidOrders: PrunedSignedOrder[];
let marketSellSwapQuote: SwapQuote;
let marketBuySwapQuote: SwapQuote;
let invalidMarketBuySwapQuote: SwapQuote;
let swapQuoteConsumer: ForwarderSwapQuoteConsumer;
let erc20ProxyAddress: string;
let expectMakerAndTakerBalancesAsync: (
expectedMakerBalance: BigNumber,
expectedTakerBalance: BigNumber,
) => Promise<void>;
const chainId = TESTRPC_CHAIN_ID;
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(),
];
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider);
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
[makerAssetData, takerAssetData, wethAssetData] = await Promise.all([
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 () => {
await blockchainLifecycle.revertAsync();
@ -77,35 +148,48 @@ describe('ForwarderSwapQuoteConsumer', () => {
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({
await erc20TokenContract.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,
);
await erc20TokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE)
.sendTransactionAsync({ from: makerAddress });
await forwarderContract.approveMakerAssetProxy(makerAssetData).sendTransactionAsync({ from: makerAddress });
orders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...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(
makerAssetData,
wethAssetData,
orders,
MarketOperation.Sell,
GAS_PRICE,
);
marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -113,34 +197,29 @@ describe('ForwarderSwapQuoteConsumer', () => {
wethAssetData,
orders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, {
invalidMarketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidOrders,
MarketOperation.Buy,
GAS_PRICE,
);
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
chainId,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('executeSwapQuoteOrThrowAsync', () => {
describe('#executeSwapQuoteOrThrowAsync', () => {
describe('validation', () => {
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth)', async () => {
expect(
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidSwapQuote, { takerAddress }),
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }),
).to.be.rejectedWith(
`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)
* Does not test the validity of the state change performed by the forwarder smart contract
*/
it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
let takerBalance = await erc20Token.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 });
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI));
it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
});
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 () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
let takerBalance = await erc20Token.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 erc20Token.balanceOf(makerAddress).callAsync();
takerBalance = await erc20Token.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 a marketSell execution when provided a MarketSell type swapQuote', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('should perform a marketBuy execution with affiliate fees', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
takerAddress,
feePercentage: 0.05,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
feePercentage: FEE_PERCENTAGE,
feeRecipient,
});
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus(
marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInEthAmount,
);
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 () => {
// let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
// let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
// const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
// expect(makerBalance).to.bignumber.equal((new BigNumber(10)).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceBefore);
// await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, feePercentage: 0.05, feeRecipient });
// makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
// takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
// const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
// console.log(makerBalance, takerBalance, feeRecipientEthBalanceAfter);
// expect(makerBalance).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI));
// expect(takerBalance).to.bignumber.equal((new BigNumber(9.5)).multipliedBy(ONE_ETH_IN_WEI));
// expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI));
// });
it('should perform a marketSell execution with affiliate fees', async () => {
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, {
takerAddress,
feePercentage: FEE_PERCENTAGE,
feeRecipient,
gasPrice: GAS_PRICE,
gasLimit: 4000000,
});
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
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', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
expect(swapQuoteConsumer.getSmartContractParamsOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith(
expect(
swapQuoteConsumer.getSmartContractParamsOrThrowAsync(invalidMarketBuySwapQuote, {}),
).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
@ -247,9 +335,8 @@ describe('ForwarderSwapQuoteConsumer', () => {
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
expect(toAddress).to.deep.equal(forwarderContract.address);
const {
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
@ -260,17 +347,15 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0);
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
marketBuySwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
expect(toAddress).to.deep.equal(forwarderContract.address);
const {
makerAssetFillAmount,
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
@ -284,7 +369,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(0);
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with affiliate fees for a marketSell SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
@ -294,9 +378,8 @@ describe('ForwarderSwapQuoteConsumer', () => {
feeRecipient,
},
);
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
expect(toAddress).to.deep.equal(forwarderContract.address);
const {
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
@ -307,7 +390,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI));
expect(feeSignatures).to.deep.equal([]);
});
it('provide correct and optimized smart contract params with affiliate fees for a marketBuy SwapQuote', async () => {
const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(
@ -317,10 +399,9 @@ describe('ForwarderSwapQuoteConsumer', () => {
feeRecipient,
},
);
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
expect(toAddress).to.deep.equal(forwarderContract.address);
const {
makerAssetFillAmount,
feeSignatures,
feePercentage,
feeRecipient: feeRecipientFromParams,
signatures,
@ -334,29 +415,14 @@ describe('ForwarderSwapQuoteConsumer', () => {
const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature);
expect(signatures).to.deep.equal(orderSignatures);
expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI));
expect(feeSignatures).to.deep.equal([]);
});
});
});
describe('getCalldataOrThrow', () => {
describe('#getCalldataOrThrow', () => {
describe('validation', () => {
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
takerAssetData,
invalidSignedOrders,
MARKET_OPERATION,
);
expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith(
expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith(
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
);
});
@ -364,33 +430,34 @@ describe('ForwarderSwapQuoteConsumer', () => {
describe('valid swap quote', async () => {
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketSellSwapQuote,
{},
);
expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
expect(toAddress).to.deep.equal(forwarderContract.address);
await web3Wrapper.sendTransactionAsync({
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: marketSellSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI));
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
let makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
let takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(
await expectMakerAndTakerBalancesAsync(
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
constants.ZERO_AMOUNT,
);
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
marketBuySwapQuote,
{},
);
@ -399,19 +466,84 @@ describe('ForwarderSwapQuoteConsumer', () => {
from: takerAddress,
to: toAddress,
data: calldataHexString,
value: marketBuySwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount,
value: ethAmount,
gasPrice: GAS_PRICE,
gas: 4000000,
});
makerBalance = await erc20Token.balanceOf(makerAddress).callAsync();
takerBalance = await erc20Token.balanceOf(takerAddress).callAsync();
expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
await expectMakerAndTakerBalancesAsync(
constants.ZERO_AMOUNT,
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
);
});
// 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 () => {
// });
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),
);
});
});
});
// 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 { 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 { constants } from '../src/constants';
import { ExtensionContractType, MarketOperation, PrunedSignedOrder } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
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';
chaiSetup.configure();
@ -19,15 +22,52 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337;
const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
const LARGE_FILLABLE_AMOUNTS = [new BigNumber(20), new BigNumber(20), new BigNumber(20)].map(value =>
value.multipliedBy(ONE_ETH_IN_WEI),
);
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PARTIAL_PRUNED_SIGNED_ORDERS: 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(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', () => {
let contractWrappers: ContractWrappers;
let wethContract: WETH9Contract;
let userAddresses: string[];
let makerAddress: string;
let takerAddress: string;
@ -38,25 +78,48 @@ describe('swapQuoteConsumerUtils', () => {
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let swapQuoteConsumer: SwapQuoteConsumer;
let orderFactory: OrderFactory;
let forwarderOrderFactory: OrderFactory;
const chainId = TESTRPC_CHAIN_ID;
before(async () => {
contractAddresses = await migrateOnceAsync();
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const config = {
chainId,
contractAddresses,
};
contractWrappers = new ContractWrappers(provider, config);
const devUtils = new DevUtilsContract(contractAddresses.devUtils, provider);
wethContract = new WETH9Contract(contractAddresses.etherToken, provider);
[takerAddress, makerAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
await contractWrappers.devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(takerTokenAddress).callAsync(),
await contractWrappers.devUtils.encodeERC20AssetData(contractAddresses.etherToken).callAsync(),
await devUtils.encodeERC20AssetData(makerTokenAddress).callAsync(),
await devUtils.encodeERC20AssetData(takerTokenAddress).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, {
chainId,
});
@ -72,46 +135,50 @@ describe('swapQuoteConsumerUtils', () => {
});
describe('getConsumerTypeForSwapQuoteAsync', () => {
let forwarderOrders: SignedOrder[];
let exchangeOrders: SignedOrder[];
let largeForwarderOrders: SignedOrder[];
let forwarderOrders: PrunedSignedOrder[];
let exchangeOrders: PrunedSignedOrder[];
let largeForwarderOrders: PrunedSignedOrder[];
let forwarderSwapQuote: SwapQuote;
let exchangeSwapQuote: SwapQuote;
let largeForwarderSwapQuote: SwapQuote;
beforeEach(async () => {
exchangeOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
exchangeOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
exchangeOrders.push(prunedOrder as PrunedSignedOrder);
}
forwarderOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
FILLABLE_AMOUNTS,
);
forwarderOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
forwarderOrders.push(prunedOrder as PrunedSignedOrder);
}
largeForwarderOrders = await getSignedOrdersWithNoFeesAsync(
provider,
makerAssetData,
wethAssetData,
makerAddress,
takerAddress,
LARGE_FILLABLE_AMOUNTS,
);
largeForwarderOrders = [];
for (const partialOrder of PARTIAL_LARGE_PRUNED_SIGNED_ORDERS) {
const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
largeForwarderOrders.push(prunedOrder as PrunedSignedOrder);
}
forwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
makerAssetData,
wethAssetData,
forwarderOrders,
MarketOperation.Sell,
GAS_PRICE,
);
largeForwarderSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -119,6 +186,7 @@ describe('swapQuoteConsumerUtils', () => {
wethAssetData,
largeForwarderOrders,
MarketOperation.Sell,
GAS_PRICE,
);
exchangeSwapQuote = getFullyFillableSwapQuoteWithNoFees(
@ -126,6 +194,7 @@ describe('swapQuoteConsumerUtils', () => {
takerAssetData,
exchangeOrders,
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 () => {
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(
forwarderSwapQuote,
{ 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 () => {
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(
largeForwarderSwapQuote,
{ takerAddress },

View File

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

View File

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

View File

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