@0x/asset-swapper
: Rebase against development and pay protocol fees.
This commit is contained in:
parent
32258ef666
commit
439c98a6e5
@ -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",
|
||||
|
@ -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
1839
packages/asset-swapper/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": "*",
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
92
packages/asset-swapper/src/utils/market_utils.ts
Normal file
92
packages/asset-swapper/src/utils/market_utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
96
packages/asset-swapper/src/utils/order_prune_utils.ts
Normal file
96
packages/asset-swapper/src/utils/order_prune_utils.ts
Normal 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;
|
||||
}
|
||||
}
|
31
packages/asset-swapper/src/utils/protocol_fee_utils.ts
Normal file
31
packages/asset-swapper/src/utils/protocol_fee_utils.ts
Normal 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;
|
||||
},
|
||||
};
|
29
packages/asset-swapper/src/utils/sorting_utils.ts
Normal file
29
packages/asset-swapper/src/utils/sorting_utils.ts
Normal 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;
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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];
|
||||
},
|
||||
};
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
57
packages/asset-swapper/test/calculate_liquidity_test.ts
Normal file
57
packages/asset-swapper/test/calculate_liquidity_test.ts
Normal 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));
|
||||
});
|
||||
});
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
374
packages/asset-swapper/test/market_utils_test.ts
Normal file
374
packages/asset-swapper/test/market_utils_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
336
packages/asset-swapper/test/order_prune_utils_test.ts
Normal file
336
packages/asset-swapper/test/order_prune_utils_test.ts
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
134
packages/asset-swapper/test/sorting_utils_test.ts
Normal file
134
packages/asset-swapper/test/sorting_utils_test.ts
Normal 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
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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 },
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
0
packages/asset-swapper/test/utils/consumer_utils.ts
Normal file
0
packages/asset-swapper/test/utils/consumer_utils.ts
Normal 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;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
63
packages/asset-swapper/test/utils/test_order_factory.ts
Normal file
63
packages/asset-swapper/test/utils/test_order_factory.ts
Normal 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);
|
||||
}
|
146
packages/asset-swapper/test/utils/test_orders.ts
Normal file
146
packages/asset-swapper/test/utils/test_orders.ts
Normal 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,
|
||||
};
|
9
packages/asset-swapper/test/utils/utils.ts
Normal file
9
packages/asset-swapper/test/utils/utils.ts
Normal 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);
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"note": "Deploy Forwarder AFTER staking is hooked up",
|
||||
"pr": "TODO"
|
||||
"pr": 2350
|
||||
}
|
||||
]
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user