Merge branch 'development' into refactor_rfq_comparison_price_integration
This commit is contained in:
commit
4aa5a89cd7
@ -145,6 +145,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Fix exchange proxy overhead gas being scaled by gas price",
|
"note": "Fix exchange proxy overhead gas being scaled by gas price",
|
||||||
"pr": 2723
|
"pr": 2723
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Remove 0x-API swap/v0-specifc code from asset-swapper",
|
||||||
|
"pr": 2725
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
|
||||||
import { ExchangeContract } from '@0x/contract-wrappers';
|
|
||||||
import { providerUtils } from '@0x/utils';
|
|
||||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { constants } from '../constants';
|
|
||||||
import {
|
|
||||||
CalldataInfo,
|
|
||||||
MarketOperation,
|
|
||||||
SwapQuote,
|
|
||||||
SwapQuoteConsumerBase,
|
|
||||||
SwapQuoteConsumerOpts,
|
|
||||||
SwapQuoteExecutionOpts,
|
|
||||||
SwapQuoteGetOutputOpts,
|
|
||||||
} from '../types';
|
|
||||||
import { assert } from '../utils/assert';
|
|
||||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
|
|
||||||
|
|
||||||
export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|
||||||
public readonly provider: ZeroExProvider;
|
|
||||||
public readonly chainId: number;
|
|
||||||
|
|
||||||
private readonly _exchangeContract: ExchangeContract;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
supportedProvider: SupportedProvider,
|
|
||||||
public readonly 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._exchangeContract = new ExchangeContract(contractAddresses.exchange, supportedProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getCalldataOrThrowAsync(
|
|
||||||
quote: SwapQuote,
|
|
||||||
_opts: Partial<SwapQuoteGetOutputOpts> = {},
|
|
||||||
): Promise<CalldataInfo> {
|
|
||||||
assert.isValidSwapQuote('quote', quote);
|
|
||||||
const { orders } = quote;
|
|
||||||
const signatures = _.map(orders, o => o.signature);
|
|
||||||
|
|
||||||
let calldataHexString;
|
|
||||||
if (quote.type === MarketOperation.Buy) {
|
|
||||||
calldataHexString = this._exchangeContract
|
|
||||||
.marketBuyOrdersFillOrKill(orders, quote.makerAssetFillAmount, signatures)
|
|
||||||
.getABIEncodedTransactionData();
|
|
||||||
} else {
|
|
||||||
calldataHexString = this._exchangeContract
|
|
||||||
.marketSellOrdersFillOrKill(orders, quote.takerAssetFillAmount, signatures)
|
|
||||||
.getABIEncodedTransactionData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
calldataHexString,
|
|
||||||
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
|
|
||||||
toAddress: this._exchangeContract.address,
|
|
||||||
allowanceTarget: this.contractAddresses.erc20Proxy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async executeSwapQuoteOrThrowAsync(
|
|
||||||
quote: SwapQuote,
|
|
||||||
opts: Partial<SwapQuoteExecutionOpts>,
|
|
||||||
): Promise<string> {
|
|
||||||
assert.isValidSwapQuote('quote', quote);
|
|
||||||
|
|
||||||
const { takerAddress, gasLimit, ethAmount } = opts;
|
|
||||||
|
|
||||||
if (takerAddress !== undefined) {
|
|
||||||
assert.isETHAddressHex('takerAddress', takerAddress);
|
|
||||||
}
|
|
||||||
if (gasLimit !== undefined) {
|
|
||||||
assert.isNumber('gasLimit', gasLimit);
|
|
||||||
}
|
|
||||||
if (ethAmount !== undefined) {
|
|
||||||
assert.isBigNumber('ethAmount', ethAmount);
|
|
||||||
}
|
|
||||||
const { orders, gasPrice } = quote;
|
|
||||||
const signatures = orders.map(o => o.signature);
|
|
||||||
|
|
||||||
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
|
|
||||||
const value = ethAmount || quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
|
|
||||||
let txHash: string;
|
|
||||||
if (quote.type === MarketOperation.Buy) {
|
|
||||||
const { makerAssetFillAmount } = quote;
|
|
||||||
txHash = await this._exchangeContract
|
|
||||||
.marketBuyOrdersFillOrKill(orders, makerAssetFillAmount, signatures)
|
|
||||||
.sendTransactionAsync({
|
|
||||||
from: finalTakerAddress,
|
|
||||||
gas: gasLimit,
|
|
||||||
gasPrice,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { takerAssetFillAmount } = quote;
|
|
||||||
txHash = await this._exchangeContract
|
|
||||||
.marketSellOrdersFillOrKill(orders, takerAssetFillAmount, signatures)
|
|
||||||
.sendTransactionAsync({
|
|
||||||
from: finalTakerAddress,
|
|
||||||
gas: gasLimit,
|
|
||||||
gasPrice,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// TODO(dorothy-zbornak): Handle signature request denied
|
|
||||||
// (see contract-wrappers/decorators)
|
|
||||||
// and ExchangeRevertErrors.IncompleteFillError.
|
|
||||||
return txHash;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,198 +0,0 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
|
||||||
import { ForwarderContract } from '@0x/contract-wrappers';
|
|
||||||
import { assetDataUtils } from '@0x/order-utils';
|
|
||||||
import { providerUtils } from '@0x/utils';
|
|
||||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { constants } from '../constants';
|
|
||||||
import {
|
|
||||||
CalldataInfo,
|
|
||||||
MarketOperation,
|
|
||||||
SwapQuote,
|
|
||||||
SwapQuoteConsumerBase,
|
|
||||||
SwapQuoteConsumerOpts,
|
|
||||||
SwapQuoteExecutionOpts,
|
|
||||||
SwapQuoteGetOutputOpts,
|
|
||||||
} from '../types';
|
|
||||||
import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
|
|
||||||
import { assert } from '../utils/assert';
|
|
||||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
|
|
||||||
|
|
||||||
const { NULL_ADDRESS } = constants;
|
|
||||||
|
|
||||||
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|
||||||
public readonly provider: ZeroExProvider;
|
|
||||||
public readonly chainId: number;
|
|
||||||
public buyQuoteSellAmountScalingFactor = 1.0001; // 100% + 1 bps
|
|
||||||
|
|
||||||
private readonly _forwarder: ForwarderContract;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
supportedProvider: SupportedProvider,
|
|
||||||
public readonly 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._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a SwapQuote, returns 'CalldataInfo' for a forwarder extension 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 CalldataInfo. See type definition for more information.
|
|
||||||
*/
|
|
||||||
public async getCalldataOrThrowAsync(
|
|
||||||
quote: SwapQuote,
|
|
||||||
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
|
||||||
): Promise<CalldataInfo> {
|
|
||||||
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
|
|
||||||
const { extensionContractOpts } = { ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, ...opts };
|
|
||||||
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
|
|
||||||
const { feeRecipient, feePercentage } = extensionContractOpts;
|
|
||||||
const { orders, worstCaseQuoteInfo } = quote;
|
|
||||||
|
|
||||||
const normalizedFeeRecipientAddress = feeRecipient.toLowerCase();
|
|
||||||
const signatures = _.map(orders, o => o.signature);
|
|
||||||
const ethAmountWithFees = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(
|
|
||||||
{
|
|
||||||
...worstCaseQuoteInfo,
|
|
||||||
// HACK(dorothy-zbornak): The forwarder contract has a rounding bug
|
|
||||||
// that causes buys of low-decimal tokens to not complete.
|
|
||||||
// Scaling the max sell amount by 1bps seems to be sufficient to
|
|
||||||
// overcome this.
|
|
||||||
...(quote.type === MarketOperation.Buy
|
|
||||||
? {
|
|
||||||
// tslint:disable-next-line: custom-no-magic-numbers
|
|
||||||
totalTakerAssetAmount: worstCaseQuoteInfo.totalTakerAssetAmount
|
|
||||||
.times(this.buyQuoteSellAmountScalingFactor)
|
|
||||||
.integerValue(),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
feePercentage,
|
|
||||||
);
|
|
||||||
const feeAmount = affiliateFeeUtils.getFeeAmount(worstCaseQuoteInfo, feePercentage);
|
|
||||||
|
|
||||||
let calldataHexString;
|
|
||||||
if (quote.type === MarketOperation.Buy) {
|
|
||||||
calldataHexString = this._forwarder
|
|
||||||
.marketBuyOrdersWithEth(
|
|
||||||
orders,
|
|
||||||
quote.makerAssetFillAmount,
|
|
||||||
signatures,
|
|
||||||
[feeAmount],
|
|
||||||
[normalizedFeeRecipientAddress],
|
|
||||||
)
|
|
||||||
.getABIEncodedTransactionData();
|
|
||||||
} else {
|
|
||||||
calldataHexString = this._forwarder
|
|
||||||
.marketSellAmountWithEth(
|
|
||||||
orders,
|
|
||||||
quote.takerAssetFillAmount,
|
|
||||||
signatures,
|
|
||||||
[feeAmount],
|
|
||||||
[normalizedFeeRecipientAddress],
|
|
||||||
)
|
|
||||||
.getABIEncodedTransactionData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
calldataHexString,
|
|
||||||
toAddress: this._forwarder.address,
|
|
||||||
ethAmount: ethAmountWithFees,
|
|
||||||
allowanceTarget: NULL_ADDRESS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a SwapQuote and desired rate (in Eth), attempt to execute the swap.
|
|
||||||
* @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>,
|
|
||||||
): Promise<string> {
|
|
||||||
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
|
|
||||||
|
|
||||||
const { ethAmount: providedEthAmount, takerAddress, gasLimit, extensionContractOpts } = {
|
|
||||||
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
|
||||||
...opts,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts);
|
|
||||||
|
|
||||||
const { feeRecipient, feePercentage } = extensionContractOpts;
|
|
||||||
|
|
||||||
if (providedEthAmount !== undefined) {
|
|
||||||
assert.isBigNumber('ethAmount', providedEthAmount);
|
|
||||||
}
|
|
||||||
if (takerAddress !== undefined) {
|
|
||||||
assert.isETHAddressHex('takerAddress', takerAddress);
|
|
||||||
}
|
|
||||||
if (gasLimit !== undefined) {
|
|
||||||
assert.isNumber('gasLimit', gasLimit);
|
|
||||||
}
|
|
||||||
const { orders, gasPrice } = quote; // tslint:disable-line:no-unused-variable
|
|
||||||
const signatures = orders.map(o => o.signature);
|
|
||||||
|
|
||||||
// get taker address
|
|
||||||
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
|
|
||||||
// if no ethAmount is provided, default to the worst totalTakerAssetAmount
|
|
||||||
const ethAmountWithFees =
|
|
||||||
providedEthAmount ||
|
|
||||||
affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(quote.worstCaseQuoteInfo, feePercentage);
|
|
||||||
const feeAmount = affiliateFeeUtils.getFeeAmount(
|
|
||||||
{
|
|
||||||
...quote.worstCaseQuoteInfo,
|
|
||||||
// HACK(dorothy-zbornak): The forwarder contract has a rounding bug
|
|
||||||
// that causes buys of low-decimal tokens to not complete.
|
|
||||||
// Scaling the max sell amount by 1bps seems to be sufficient to
|
|
||||||
// overcome this.
|
|
||||||
...(quote.type === MarketOperation.Buy
|
|
||||||
? {
|
|
||||||
// tslint:disable-next-line: custom-no-magic-numbers
|
|
||||||
totalTakerAssetAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount
|
|
||||||
.times(this.buyQuoteSellAmountScalingFactor)
|
|
||||||
.integerValue(),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
feePercentage,
|
|
||||||
);
|
|
||||||
let txHash: string;
|
|
||||||
if (quote.type === MarketOperation.Buy) {
|
|
||||||
const { makerAssetFillAmount } = quote;
|
|
||||||
txHash = await this._forwarder
|
|
||||||
.marketBuyOrdersWithEth(orders, makerAssetFillAmount, signatures, [feeAmount], [feeRecipient])
|
|
||||||
.sendTransactionAsync({
|
|
||||||
from: finalTakerAddress,
|
|
||||||
gas: gasLimit,
|
|
||||||
gasPrice,
|
|
||||||
value: ethAmountWithFees,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
txHash = await this._forwarder
|
|
||||||
.marketSellAmountWithEth(orders, quote.takerAssetFillAmount, signatures, [feeAmount], [feeRecipient])
|
|
||||||
.sendTransactionAsync({
|
|
||||||
from: finalTakerAddress,
|
|
||||||
gas: gasLimit,
|
|
||||||
gasPrice,
|
|
||||||
value: ethAmountWithFees,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// TODO(dorothy-zbornak): Handle signature request denied
|
|
||||||
// (see contract-wrappers/decorators)
|
|
||||||
// and ForwarderRevertErrors.CompleteBuyFailed.
|
|
||||||
return txHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getEtherTokenAssetDataOrThrow(): string {
|
|
||||||
return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,15 +18,11 @@ import { assert } from '../utils/assert';
|
|||||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
|
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
|
||||||
|
|
||||||
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
|
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
|
||||||
import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer';
|
|
||||||
import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer';
|
|
||||||
|
|
||||||
export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||||
public readonly provider: ZeroExProvider;
|
public readonly provider: ZeroExProvider;
|
||||||
public readonly chainId: number;
|
public readonly chainId: number;
|
||||||
|
|
||||||
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
|
|
||||||
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
|
|
||||||
private readonly _contractAddresses: ContractAddresses;
|
private readonly _contractAddresses: ContractAddresses;
|
||||||
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
|
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
|
||||||
|
|
||||||
@ -45,8 +41,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.chainId = chainId;
|
this.chainId = chainId;
|
||||||
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
|
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
|
||||||
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
|
|
||||||
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
|
|
||||||
this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
|
this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
|
||||||
supportedProvider,
|
supportedProvider,
|
||||||
this._contractAddresses,
|
this._contractAddresses,
|
||||||
@ -100,13 +94,12 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
|
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
|
||||||
|
// ( akroeger)leaving this switch to use different contracts in the future
|
||||||
switch (opts.useExtensionContract) {
|
switch (opts.useExtensionContract) {
|
||||||
case ExtensionContractType.Forwarder:
|
|
||||||
return this._forwarderConsumer;
|
|
||||||
case ExtensionContractType.ExchangeProxy:
|
case ExtensionContractType.ExchangeProxy:
|
||||||
return this._exchangeProxyConsumer;
|
return this._exchangeProxyConsumer;
|
||||||
default:
|
default:
|
||||||
return this._exchangeConsumer;
|
return this._exchangeProxyConsumer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,6 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
|||||||
gasSchedule: {},
|
gasSchedule: {},
|
||||||
exchangeProxyOverhead: () => ZERO_AMOUNT,
|
exchangeProxyOverhead: () => ZERO_AMOUNT,
|
||||||
allowFallback: true,
|
allowFallback: true,
|
||||||
shouldBatchBridgeOrders: true,
|
|
||||||
shouldGenerateQuoteReport: false,
|
shouldGenerateQuoteReport: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -459,7 +459,6 @@ export class MarketOperationUtils {
|
|||||||
excludedSources: _opts.excludedSources,
|
excludedSources: _opts.excludedSources,
|
||||||
feeSchedule: _opts.feeSchedule,
|
feeSchedule: _opts.feeSchedule,
|
||||||
allowFallback: _opts.allowFallback,
|
allowFallback: _opts.allowFallback,
|
||||||
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return optimizedOrders;
|
return optimizedOrders;
|
||||||
@ -497,7 +496,6 @@ export class MarketOperationUtils {
|
|||||||
orderDomain: this._orderDomain,
|
orderDomain: this._orderDomain,
|
||||||
contractAddresses: this.contractAddresses,
|
contractAddresses: this.contractAddresses,
|
||||||
bridgeSlippage: opts.bridgeSlippage || 0,
|
bridgeSlippage: opts.bridgeSlippage || 0,
|
||||||
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert native orders and dex quotes into `Fill` objects.
|
// Convert native orders and dex quotes into `Fill` objects.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
import { ContractAddresses } from '@0x/contract-addresses';
|
||||||
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
|
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
|
||||||
import { RFQTIndicativeQuote } from '@0x/quote-server';
|
import { RFQTIndicativeQuote } from '@0x/quote-server';
|
||||||
import { ERC20BridgeAssetData, SignedOrder } from '@0x/types';
|
import { SignedOrder } from '@0x/types';
|
||||||
import { AbiEncoder, BigNumber } from '@0x/utils';
|
import { AbiEncoder, BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||||
@ -40,30 +40,6 @@ import {
|
|||||||
|
|
||||||
// tslint:disable completed-docs no-unnecessary-type-assertion
|
// tslint:disable completed-docs no-unnecessary-type-assertion
|
||||||
|
|
||||||
interface DexForwaderBridgeData {
|
|
||||||
inputToken: string;
|
|
||||||
calls: Array<{
|
|
||||||
target: string;
|
|
||||||
inputTokenAmount: BigNumber;
|
|
||||||
outputTokenAmount: BigNumber;
|
|
||||||
bridgeData: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dexForwarderBridgeDataEncoder = AbiEncoder.create([
|
|
||||||
{ name: 'inputToken', type: 'address' },
|
|
||||||
{
|
|
||||||
name: 'calls',
|
|
||||||
type: 'tuple[]',
|
|
||||||
components: [
|
|
||||||
{ name: 'target', type: 'address' },
|
|
||||||
{ name: 'inputTokenAmount', type: 'uint256' },
|
|
||||||
{ name: 'outputTokenAmount', type: 'uint256' },
|
|
||||||
{ name: 'bridgeData', type: 'bytes' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function createDummyOrderForSampler(
|
export function createDummyOrderForSampler(
|
||||||
makerAssetData: string,
|
makerAssetData: string,
|
||||||
takerAssetData: string,
|
takerAssetData: string,
|
||||||
@ -159,7 +135,6 @@ export interface CreateOrderFromPathOpts {
|
|||||||
orderDomain: OrderDomain;
|
orderDomain: OrderDomain;
|
||||||
contractAddresses: ContractAddresses;
|
contractAddresses: ContractAddresses;
|
||||||
bridgeSlippage: number;
|
bridgeSlippage: number;
|
||||||
shouldBatchBridgeOrders: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrdersFromTwoHopSample(
|
export function createOrdersFromTwoHopSample(
|
||||||
@ -340,47 +315,6 @@ export function createBridgeOrder(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
|
||||||
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
|
||||||
let totalMakerAssetAmount = ZERO_AMOUNT;
|
|
||||||
let totalTakerAssetAmount = ZERO_AMOUNT;
|
|
||||||
const batchedBridgeData: DexForwaderBridgeData = {
|
|
||||||
inputToken: takerToken,
|
|
||||||
calls: [],
|
|
||||||
};
|
|
||||||
for (const fill of fills) {
|
|
||||||
const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts);
|
|
||||||
totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount);
|
|
||||||
totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount);
|
|
||||||
const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow(
|
|
||||||
bridgeOrder.makerAssetData,
|
|
||||||
) as ERC20BridgeAssetData;
|
|
||||||
batchedBridgeData.calls.push({
|
|
||||||
target: bridgeAddress,
|
|
||||||
bridgeData: orderBridgeData,
|
|
||||||
inputTokenAmount: bridgeOrder.takerAssetAmount,
|
|
||||||
outputTokenAmount: bridgeOrder.makerAssetAmount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge;
|
|
||||||
const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
|
|
||||||
makerToken,
|
|
||||||
batchedBridgeAddress,
|
|
||||||
dexForwarderBridgeDataEncoder.encode(batchedBridgeData),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
fills,
|
|
||||||
makerAssetData: batchedMakerAssetData,
|
|
||||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
|
||||||
makerAddress: batchedBridgeAddress,
|
|
||||||
makerAssetAmount: totalMakerAssetAmount,
|
|
||||||
takerAssetAmount: totalTakerAssetAmount,
|
|
||||||
fillableMakerAssetAmount: totalMakerAssetAmount,
|
|
||||||
fillableTakerAssetAmount: totalTakerAssetAmount,
|
|
||||||
...createCommonBridgeOrderFields(opts.orderDomain),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
|
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
|
||||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
||||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
||||||
|
@ -3,13 +3,7 @@ import { BigNumber } from '@0x/utils';
|
|||||||
import { MarketOperation } from '../../types';
|
import { MarketOperation } from '../../types';
|
||||||
|
|
||||||
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
|
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
|
||||||
import {
|
import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders';
|
||||||
createBatchedBridgeOrder,
|
|
||||||
createBridgeOrder,
|
|
||||||
createNativeOrder,
|
|
||||||
CreateOrderFromPathOpts,
|
|
||||||
getMakerTakerTokens,
|
|
||||||
} from './orders';
|
|
||||||
import { getCompleteRate, getRate } from './rate_utils';
|
import { getCompleteRate, getRate } from './rate_utils';
|
||||||
import {
|
import {
|
||||||
CollapsedFill,
|
CollapsedFill,
|
||||||
@ -123,14 +117,9 @@ export class Path {
|
|||||||
}
|
}
|
||||||
contiguousBridgeFills.push(collapsedFills[j]);
|
contiguousBridgeFills.push(collapsedFills[j]);
|
||||||
}
|
}
|
||||||
// Always use DexForwarderBridge unless configured not to
|
|
||||||
if (!opts.shouldBatchBridgeOrders) {
|
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
|
||||||
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
|
i += 1;
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
this.orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
|
|
||||||
i += contiguousBridgeFills.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this as CollapsedPath;
|
return this as CollapsedPath;
|
||||||
}
|
}
|
||||||
|
@ -289,11 +289,6 @@ export interface GetMarketOrdersOpts {
|
|||||||
*/
|
*/
|
||||||
allowFallback: boolean;
|
allowFallback: boolean;
|
||||||
rfqt?: GetMarketOrdersRfqtOpts;
|
rfqt?: GetMarketOrdersRfqtOpts;
|
||||||
/**
|
|
||||||
* Whether to combine contiguous bridge orders into a single DexForwarderBridge
|
|
||||||
* order. Defaults to `true`.
|
|
||||||
*/
|
|
||||||
shouldBatchBridgeOrders: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Whether to generate a quote report
|
* Whether to generate a quote report
|
||||||
*/
|
*/
|
||||||
|
@ -1,289 +0,0 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
|
||||||
import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
|
|
||||||
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
|
|
||||||
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
|
|
||||||
import { migrateOnceAsync } from '@0x/migrations';
|
|
||||||
import { assetDataUtils } from '@0x/order-utils';
|
|
||||||
import { BigNumber } from '@0x/utils';
|
|
||||||
import * as chai from 'chai';
|
|
||||||
import 'mocha';
|
|
||||||
|
|
||||||
import { SwapQuote } from '../src';
|
|
||||||
import { constants } from '../src/constants';
|
|
||||||
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
|
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
|
||||||
|
|
||||||
import { chaiSetup } from './utils/chai_setup';
|
|
||||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } 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 = 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<SignedOrderWithFillableAmounts>> = [
|
|
||||||
{
|
|
||||||
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 userAddresses: string[];
|
|
||||||
let erc20MakerTokenContract: ERC20TokenContract;
|
|
||||||
let erc20TakerTokenContract: ERC20TokenContract;
|
|
||||||
let coinbaseAddress: string;
|
|
||||||
let makerAddress: string;
|
|
||||||
let takerAddress: string;
|
|
||||||
let orderFactory: OrderFactory;
|
|
||||||
let feeRecipient: string;
|
|
||||||
let makerTokenAddress: string;
|
|
||||||
let takerTokenAddress: string;
|
|
||||||
let makerAssetData: string;
|
|
||||||
let takerAssetData: string;
|
|
||||||
let contractAddresses: ContractAddresses;
|
|
||||||
let exchangeContract: ExchangeContract;
|
|
||||||
|
|
||||||
const chainId = TESTRPC_CHAIN_ID;
|
|
||||||
|
|
||||||
let orders: SignedOrderWithFillableAmounts[];
|
|
||||||
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(provider);
|
|
||||||
await blockchainLifecycle.startAsync();
|
|
||||||
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
|
|
||||||
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
|
|
||||||
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
|
|
||||||
[makerAssetData, takerAssetData] = [
|
|
||||||
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
|
|
||||||
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
|
|
||||||
];
|
|
||||||
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,
|
|
||||||
makerAddress,
|
|
||||||
takerAddress,
|
|
||||||
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);
|
|
||||||
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
|
|
||||||
erc20TakerTokenContract,
|
|
||||||
makerAddress,
|
|
||||||
takerAddress,
|
|
||||||
);
|
|
||||||
expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
|
|
||||||
erc20MakerTokenContract,
|
|
||||||
makerAddress,
|
|
||||||
takerAddress,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
after(async () => {
|
|
||||||
await blockchainLifecycle.revertAsync();
|
|
||||||
});
|
|
||||||
beforeEach(async () => {
|
|
||||||
await blockchainLifecycle.startAsync();
|
|
||||||
orders = [];
|
|
||||||
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
|
|
||||||
const order = await orderFactory.newSignedOrderAsync(partialOrder);
|
|
||||||
const prunedOrder = {
|
|
||||||
...order,
|
|
||||||
...partialOrder,
|
|
||||||
};
|
|
||||||
orders.push(prunedOrder as SignedOrderWithFillableAmounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
|
||||||
makerAssetData,
|
|
||||||
takerAssetData,
|
|
||||||
orders,
|
|
||||||
MarketOperation.Sell,
|
|
||||||
GAS_PRICE,
|
|
||||||
);
|
|
||||||
|
|
||||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
|
||||||
makerAssetData,
|
|
||||||
takerAssetData,
|
|
||||||
orders,
|
|
||||||
MarketOperation.Buy,
|
|
||||||
GAS_PRICE,
|
|
||||||
);
|
|
||||||
|
|
||||||
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', () => {
|
|
||||||
/*
|
|
||||||
* 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 () => {
|
|
||||||
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,
|
|
||||||
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 () => {
|
|
||||||
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,
|
|
||||||
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('#getCalldataOrThrow', () => {
|
|
||||||
describe('valid swap quote', async () => {
|
|
||||||
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
|
|
||||||
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(exchangeContract.address);
|
|
||||||
await web3Wrapper.sendTransactionAsync({
|
|
||||||
from: takerAddress,
|
|
||||||
to: toAddress,
|
|
||||||
data: calldataHexString,
|
|
||||||
gas: 4000000,
|
|
||||||
gasPrice: GAS_PRICE,
|
|
||||||
value: ethAmount,
|
|
||||||
});
|
|
||||||
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 () => {
|
|
||||||
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(exchangeContract.address);
|
|
||||||
await web3Wrapper.sendTransactionAsync({
|
|
||||||
from: takerAddress,
|
|
||||||
to: toAddress,
|
|
||||||
data: calldataHexString,
|
|
||||||
gas: 4000000,
|
|
||||||
gasPrice: GAS_PRICE,
|
|
||||||
value: ethAmount,
|
|
||||||
});
|
|
||||||
await expectMakerAndTakerBalancesForMakerAssetAsync(
|
|
||||||
constants.ZERO_AMOUNT,
|
|
||||||
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,440 +0,0 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
|
||||||
import { ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers';
|
|
||||||
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
|
|
||||||
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
|
|
||||||
import { migrateOnceAsync } from '@0x/migrations';
|
|
||||||
import { assetDataUtils } from '@0x/order-utils';
|
|
||||||
import { BigNumber } from '@0x/utils';
|
|
||||||
import * as chai from 'chai';
|
|
||||||
import 'mocha';
|
|
||||||
|
|
||||||
import { SwapQuote } from '../src';
|
|
||||||
import { constants } from '../src/constants';
|
|
||||||
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
|
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
|
||||||
|
|
||||||
import { chaiSetup } from './utils/chai_setup';
|
|
||||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } 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 = devConstants.TESTRPC_CHAIN_ID;
|
|
||||||
|
|
||||||
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
|
|
||||||
const FEE_PERCENTAGE = 0.05;
|
|
||||||
const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array<Partial<SignedOrderWithFillableAmounts>> = [
|
|
||||||
{
|
|
||||||
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 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 invalidOrderFactory: OrderFactory;
|
|
||||||
let wethAssetData: string;
|
|
||||||
let contractAddresses: ContractAddresses;
|
|
||||||
let erc20TokenContract: ERC20TokenContract;
|
|
||||||
let forwarderContract: ForwarderContract;
|
|
||||||
|
|
||||||
let orders: SignedOrderWithFillableAmounts[];
|
|
||||||
let invalidOrders: SignedOrderWithFillableAmounts[];
|
|
||||||
let marketSellSwapQuote: SwapQuote;
|
|
||||||
let marketBuySwapQuote: SwapQuote;
|
|
||||||
let invalidMarketBuySwapQuote: SwapQuote;
|
|
||||||
let swapQuoteConsumer: ForwarderSwapQuoteConsumer;
|
|
||||||
let expectMakerAndTakerBalancesAsync: (
|
|
||||||
expectedMakerBalance: BigNumber,
|
|
||||||
expectedTakerBalance: BigNumber,
|
|
||||||
) => Promise<void>;
|
|
||||||
const chainId = TESTRPC_CHAIN_ID;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
contractAddresses = await migrateOnceAsync(provider);
|
|
||||||
await blockchainLifecycle.startAsync();
|
|
||||||
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
|
|
||||||
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
|
|
||||||
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
|
|
||||||
erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider);
|
|
||||||
forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider);
|
|
||||||
[makerAssetData, takerAssetData, wethAssetData] = [
|
|
||||||
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
|
|
||||||
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
|
|
||||||
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
|
|
||||||
];
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
beforeEach(async () => {
|
|
||||||
await blockchainLifecycle.startAsync();
|
|
||||||
const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
|
|
||||||
|
|
||||||
const totalFillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
|
|
||||||
|
|
||||||
await erc20TokenContract.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({
|
|
||||||
from: coinbaseAddress,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 SignedOrderWithFillableAmounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidOrders = [];
|
|
||||||
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) {
|
|
||||||
const order = await invalidOrderFactory.newSignedOrderAsync(partialOrder);
|
|
||||||
const prunedOrder = {
|
|
||||||
...order,
|
|
||||||
...partialOrder,
|
|
||||||
};
|
|
||||||
invalidOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
|
||||||
makerAssetData,
|
|
||||||
wethAssetData,
|
|
||||||
orders,
|
|
||||||
MarketOperation.Sell,
|
|
||||||
GAS_PRICE,
|
|
||||||
);
|
|
||||||
|
|
||||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
|
||||||
makerAssetData,
|
|
||||||
wethAssetData,
|
|
||||||
orders,
|
|
||||||
MarketOperation.Buy,
|
|
||||||
GAS_PRICE,
|
|
||||||
);
|
|
||||||
|
|
||||||
invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
|
||||||
makerAssetData,
|
|
||||||
takerAssetData,
|
|
||||||
invalidOrders,
|
|
||||||
MarketOperation.Buy,
|
|
||||||
GAS_PRICE,
|
|
||||||
);
|
|
||||||
|
|
||||||
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
|
|
||||||
chainId,
|
|
||||||
});
|
|
||||||
swapQuoteConsumer.buyQuoteSellAmountScalingFactor = 1;
|
|
||||||
});
|
|
||||||
afterEach(async () => {
|
|
||||||
await blockchainLifecycle.revertAsync();
|
|
||||||
});
|
|
||||||
describe('#executeSwapQuoteOrThrowAsync', () => {
|
|
||||||
describe('validation', () => {
|
|
||||||
it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth)', async () => {
|
|
||||||
expect(
|
|
||||||
swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }),
|
|
||||||
).to.be.rejectedWith(
|
|
||||||
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO(david) test execution of swap quotes with fee orders
|
|
||||||
describe('valid swap quote', () => {
|
|
||||||
/*
|
|
||||||
* Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
|
|
||||||
* Does not test the validity of the state change performed by the forwarder smart contract
|
|
||||||
*/
|
|
||||||
it('should perform a 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,
|
|
||||||
gasLimit: 4000000,
|
|
||||||
ethAmount: new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
|
|
||||||
});
|
|
||||||
await expectMakerAndTakerBalancesAsync(
|
|
||||||
constants.ZERO_AMOUNT,
|
|
||||||
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
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 () => {
|
|
||||||
await expectMakerAndTakerBalancesAsync(
|
|
||||||
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
|
|
||||||
constants.ZERO_AMOUNT,
|
|
||||||
);
|
|
||||||
const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient);
|
|
||||||
await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, {
|
|
||||||
takerAddress,
|
|
||||||
gasLimit: 4000000,
|
|
||||||
extensionContractOpts: {
|
|
||||||
feePercentage: 0.05,
|
|
||||||
feeRecipient,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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.protocolFeeInWeiAmount,
|
|
||||||
);
|
|
||||||
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
|
|
||||||
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
gasLimit: 4000000,
|
|
||||||
extensionContractOpts: {
|
|
||||||
feePercentage: 0.05,
|
|
||||||
feeRecipient,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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.protocolFeeInWeiAmount,
|
|
||||||
);
|
|
||||||
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
|
|
||||||
new BigNumber(FEE_PERCENTAGE).times(totalEthSpent),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getCalldataOrThrow', () => {
|
|
||||||
describe('validation', () => {
|
|
||||||
it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => {
|
|
||||||
expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith(
|
|
||||||
`Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('valid swap quote', async () => {
|
|
||||||
it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => {
|
|
||||||
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(forwarderContract.address);
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => {
|
|
||||||
await expectMakerAndTakerBalancesAsync(
|
|
||||||
new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI),
|
|
||||||
constants.ZERO_AMOUNT,
|
|
||||||
);
|
|
||||||
const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync(
|
|
||||||
marketBuySwapQuote,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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,
|
|
||||||
{
|
|
||||||
extensionContractOpts: {
|
|
||||||
feePercentage: 0.05,
|
|
||||||
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.protocolFeeInWeiAmount,
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
{
|
|
||||||
extensionContractOpts: {
|
|
||||||
feePercentage: 0.05,
|
|
||||||
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.protocolFeeInWeiAmount,
|
|
||||||
);
|
|
||||||
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
|
|
||||||
});
|
|
@ -298,23 +298,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
return rates;
|
return rates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedOrderSources(side: MarketOperation, orders: OptimizedMarketOrder[]): ERC20BridgeSource[][] {
|
|
||||||
return (
|
|
||||||
orders
|
|
||||||
// Sort fills by descending rate.
|
|
||||||
.map(o => {
|
|
||||||
return o.fills
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) =>
|
|
||||||
side === MarketOperation.Sell
|
|
||||||
? b.output.div(b.input).comparedTo(a.output.div(a.input))
|
|
||||||
: b.input.div(b.output).comparedTo(a.input.div(a.output)),
|
|
||||||
)
|
|
||||||
.map(f => f.source);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NUM_SAMPLES = 3;
|
const NUM_SAMPLES = 3;
|
||||||
|
|
||||||
interface RatesBySource {
|
interface RatesBySource {
|
||||||
@ -507,7 +490,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
maxFallbackSlippage: 100,
|
maxFallbackSlippage: 100,
|
||||||
excludedSources: DEFAULT_EXCLUDED,
|
excludedSources: DEFAULT_EXCLUDED,
|
||||||
allowFallback: false,
|
allowFallback: false,
|
||||||
shouldBatchBridgeOrders: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1273,7 +1255,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor),
|
excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor),
|
||||||
numSamples: 4,
|
numSamples: 4,
|
||||||
bridgeSlippage: 0,
|
bridgeSlippage: 0,
|
||||||
shouldBatchBridgeOrders: false,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const result = ordersAndReport.optimizedOrders;
|
const result = ordersAndReport.optimizedOrders;
|
||||||
@ -1291,37 +1272,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
|
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('batches contiguous bridge sources', async () => {
|
|
||||||
const rates: RatesBySource = {};
|
|
||||||
rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01];
|
|
||||||
rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01];
|
|
||||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
|
|
||||||
rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01];
|
|
||||||
replaceSamplerOps({
|
|
||||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
|
||||||
});
|
|
||||||
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
||||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
||||||
FILL_AMOUNT,
|
|
||||||
{
|
|
||||||
...DEFAULT_OPTS,
|
|
||||||
numSamples: 4,
|
|
||||||
excludedSources: [
|
|
||||||
ERC20BridgeSource.Kyber,
|
|
||||||
..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve),
|
|
||||||
],
|
|
||||||
shouldBatchBridgeOrders: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const improvedOrders = improvedOrdersResponse.optimizedOrders;
|
|
||||||
expect(improvedOrders).to.be.length(3);
|
|
||||||
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
|
|
||||||
expect(orderFillSources).to.deep.eq([
|
|
||||||
[ERC20BridgeSource.Uniswap],
|
|
||||||
[ERC20BridgeSource.Native],
|
|
||||||
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
it('factors in exchange proxy gas overhead', async () => {
|
it('factors in exchange proxy gas overhead', async () => {
|
||||||
// Uniswap has a slightly better rate than LiquidityProvider,
|
// Uniswap has a slightly better rate than LiquidityProvider,
|
||||||
// but LiquidityProvider is better accounting for the EP gas overhead.
|
// but LiquidityProvider is better accounting for the EP gas overhead.
|
||||||
@ -1380,7 +1330,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
maxFallbackSlippage: 100,
|
maxFallbackSlippage: 100,
|
||||||
excludedSources: DEFAULT_EXCLUDED,
|
excludedSources: DEFAULT_EXCLUDED,
|
||||||
allowFallback: false,
|
allowFallback: false,
|
||||||
shouldBatchBridgeOrders: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1732,31 +1681,6 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('batches contiguous bridge sources', async () => {
|
|
||||||
const rates: RatesBySource = { ...ZERO_RATES };
|
|
||||||
rates[ERC20BridgeSource.Native] = [0.3, 0.01, 0.01, 0.01];
|
|
||||||
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01];
|
|
||||||
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01];
|
|
||||||
replaceSamplerOps({
|
|
||||||
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
|
|
||||||
});
|
|
||||||
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
|
|
||||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
||||||
FILL_AMOUNT,
|
|
||||||
{
|
|
||||||
...DEFAULT_OPTS,
|
|
||||||
numSamples: 4,
|
|
||||||
shouldBatchBridgeOrders: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const improvedOrders = improvedOrdersResponse.optimizedOrders;
|
|
||||||
expect(improvedOrders).to.be.length(2);
|
|
||||||
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
|
|
||||||
expect(orderFillSources).to.deep.eq([
|
|
||||||
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],
|
|
||||||
[ERC20BridgeSource.Native],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
it('factors in exchange proxy gas overhead', async () => {
|
it('factors in exchange proxy gas overhead', async () => {
|
||||||
// Uniswap has a slightly better rate than LiquidityProvider,
|
// Uniswap has a slightly better rate than LiquidityProvider,
|
||||||
// but LiquidityProvider is better accounting for the EP gas overhead.
|
// but LiquidityProvider is better accounting for the EP gas overhead.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user