@0x/asset-swapper: Add ExchangeProxySwapQuoteConsumer.
This commit is contained in:
@@ -89,6 +89,10 @@
|
||||
{
|
||||
"note": "Fix Uniswap V2 path ordering",
|
||||
"pr": 2601
|
||||
},
|
||||
{
|
||||
"note": "Add exchange proxy support",
|
||||
"pr": 2591
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@0x/assert": "^3.0.7",
|
||||
"@0x/contract-addresses": "^4.9.0",
|
||||
"@0x/contract-wrappers": "^13.6.3",
|
||||
"@0x/contracts-zero-ex": "^0.1.0",
|
||||
"@0x/json-schemas": "^5.0.7",
|
||||
"@0x/order-utils": "^10.2.4",
|
||||
"@0x/orderbook": "^2.2.5",
|
||||
|
||||
@@ -60,6 +60,7 @@ export {
|
||||
SwapQuoteConsumerError,
|
||||
SignedOrderWithFillableAmounts,
|
||||
SwapQuoteOrdersBreakdown,
|
||||
ExchangeProxyContractOpts,
|
||||
} from './types';
|
||||
export {
|
||||
ERC20BridgeSource,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ContractAddresses } from '@0x/contract-addresses';
|
||||
import { ITransformERC20Contract } from '@0x/contract-wrappers';
|
||||
import {
|
||||
encodeFillQuoteTransformerData,
|
||||
encodePayTakerTransformerData,
|
||||
encodeWethTransformerData,
|
||||
ETH_TOKEN_ADDRESS,
|
||||
FillQuoteTransformerSide,
|
||||
} from '@0x/contracts-zero-ex';
|
||||
import { assetDataUtils, ERC20AssetData } from '@0x/order-utils';
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import { BigNumber, providerUtils } from '@0x/utils';
|
||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from '../constants';
|
||||
import {
|
||||
CalldataInfo,
|
||||
ExchangeProxyContractOpts,
|
||||
MarketBuySwapQuote,
|
||||
MarketOperation,
|
||||
MarketSellSwapQuote,
|
||||
SwapQuote,
|
||||
SwapQuoteConsumerBase,
|
||||
SwapQuoteConsumerOpts,
|
||||
SwapQuoteExecutionOpts,
|
||||
SwapQuoteGetOutputOpts,
|
||||
} from '../types';
|
||||
import { assert } from '../utils/assert';
|
||||
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
public readonly provider: ZeroExProvider;
|
||||
public readonly chainId: number;
|
||||
|
||||
private readonly _transformFeature: ITransformERC20Contract;
|
||||
|
||||
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.contractAddresses = contractAddresses;
|
||||
this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider);
|
||||
}
|
||||
|
||||
public async getCalldataOrThrowAsync(
|
||||
quote: MarketBuySwapQuote | MarketSellSwapQuote,
|
||||
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||
): Promise<CalldataInfo> {
|
||||
assert.isValidSwapQuote('quote', quote);
|
||||
const exchangeProxyOpts = {
|
||||
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||
...{
|
||||
isFromETH: false,
|
||||
isToETH: false,
|
||||
},
|
||||
...opts,
|
||||
}.extensionContractOpts as ExchangeProxyContractOpts;
|
||||
|
||||
const sellToken = getTokenFromAssetData(quote.takerAssetData);
|
||||
const buyToken = getTokenFromAssetData(quote.makerAssetData);
|
||||
|
||||
// Build up the transforms.
|
||||
const transforms = [];
|
||||
if (exchangeProxyOpts.isFromETH) {
|
||||
// Create a WETH wrapper if coming from ETH.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.wethTransformer,
|
||||
data: encodeWethTransformerData({
|
||||
token: ETH_TOKEN_ADDRESS,
|
||||
amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// This transformer will fill the quote.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.fillQuoteTransformer,
|
||||
data: encodeFillQuoteTransformerData({
|
||||
sellToken,
|
||||
buyToken,
|
||||
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
|
||||
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
|
||||
maxOrderFillAmounts: [],
|
||||
orders: quote.orders,
|
||||
signatures: quote.orders.map(o => o.signature),
|
||||
}),
|
||||
});
|
||||
|
||||
if (exchangeProxyOpts.isToETH) {
|
||||
// Create a WETH unwrapper if going to ETH.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.wethTransformer,
|
||||
data: encodeWethTransformerData({
|
||||
token: this.contractAddresses.etherToken,
|
||||
amount: MAX_UINT256,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// The final transformer will send all funds to the taker.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.payTakerTransformer,
|
||||
data: encodePayTakerTransformerData({
|
||||
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
|
||||
amounts: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const calldataHexString = this._transformFeature
|
||||
.transformERC20(
|
||||
sellToken,
|
||||
buyToken,
|
||||
quote.worstCaseQuoteInfo.totalTakerAssetAmount,
|
||||
quote.worstCaseQuoteInfo.makerAssetAmount,
|
||||
transforms,
|
||||
)
|
||||
.getABIEncodedTransactionData();
|
||||
|
||||
return {
|
||||
calldataHexString,
|
||||
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
|
||||
toAddress: this._transformFeature.address,
|
||||
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget,
|
||||
};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public async executeSwapQuoteOrThrowAsync(
|
||||
_quote: SwapQuote,
|
||||
_opts: Partial<SwapQuoteExecutionOpts>,
|
||||
): Promise<string> {
|
||||
throw new Error('Execution not supported for Exchange Proxy quotes');
|
||||
}
|
||||
}
|
||||
|
||||
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
|
||||
return quote.type === MarketOperation.Buy;
|
||||
}
|
||||
|
||||
function getTokenFromAssetData(assetData: string): string {
|
||||
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
|
||||
if (data.assetProxyId !== AssetProxyId.ERC20) {
|
||||
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
|
||||
}
|
||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
||||
return (data as ERC20AssetData).tokenAddress;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
|
||||
constructor(
|
||||
supportedProvider: SupportedProvider,
|
||||
contractAddresses: ContractAddresses,
|
||||
public readonly contractAddresses: ContractAddresses,
|
||||
options: Partial<SwapQuoteConsumerOpts> = {},
|
||||
) {
|
||||
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
|
||||
@@ -59,6 +59,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
calldataHexString,
|
||||
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
|
||||
toAddress: this._exchangeContract.address,
|
||||
allowanceTarget: this.contractAddresses.erc20Proxy,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,17 @@ 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;
|
||||
|
||||
private readonly _contractAddresses: ContractAddresses;
|
||||
private readonly _forwarder: ForwarderContract;
|
||||
|
||||
constructor(
|
||||
supportedProvider: SupportedProvider,
|
||||
contractAddresses: ContractAddresses,
|
||||
public readonly contractAddresses: ContractAddresses,
|
||||
options: Partial<SwapQuoteConsumerOpts> = {},
|
||||
) {
|
||||
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
|
||||
@@ -36,7 +37,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
const provider = providerUtils.standardizeOrThrow(supportedProvider);
|
||||
this.provider = provider;
|
||||
this.chainId = chainId;
|
||||
this._contractAddresses = contractAddresses;
|
||||
this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
calldataHexString,
|
||||
toAddress: this._forwarder.address,
|
||||
ethAmount: ethAmountWithFees,
|
||||
allowanceTarget: NULL_ADDRESS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,6 +161,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
}
|
||||
|
||||
private _getEtherTokenAssetDataOrThrow(): string {
|
||||
return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
|
||||
return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { assert } from '../utils/assert';
|
||||
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
|
||||
|
||||
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
|
||||
import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer';
|
||||
import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer';
|
||||
|
||||
@@ -27,6 +28,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
|
||||
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
|
||||
private readonly _contractAddresses: ContractAddresses;
|
||||
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
|
||||
|
||||
public static getSwapQuoteConsumer(
|
||||
supportedProvider: SupportedProvider,
|
||||
@@ -45,6 +47,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
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(
|
||||
supportedProvider,
|
||||
this._contractAddresses,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,9 +100,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
}
|
||||
|
||||
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
|
||||
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
|
||||
return this._forwarderConsumer;
|
||||
switch (opts.useExtensionContract) {
|
||||
case ExtensionContractType.Forwarder:
|
||||
return this._forwarderConsumer;
|
||||
case ExtensionContractType.ExchangeProxy:
|
||||
return this._exchangeProxyConsumer;
|
||||
default:
|
||||
return this._exchangeConsumer;
|
||||
}
|
||||
return this._exchangeConsumer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +50,22 @@ export interface SignedOrderWithFillableAmounts extends SignedOrder {
|
||||
* calldataHexString: The hexstring of the calldata.
|
||||
* toAddress: The contract address to call.
|
||||
* ethAmount: The eth amount in wei to send with the smart contract call.
|
||||
* allowanceTarget: The address the taker should grant an allowance to.
|
||||
*/
|
||||
export interface CalldataInfo {
|
||||
calldataHexString: string;
|
||||
toAddress: string;
|
||||
ethAmount: BigNumber;
|
||||
allowanceTarget: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the varying smart contracts that can consume a valid swap quote
|
||||
*/
|
||||
export enum ExtensionContractType {
|
||||
Forwarder = 'FORWARDER',
|
||||
None = 'NONE',
|
||||
Forwarder = 'FORWARDER',
|
||||
ExchangeProxy = 'EXCHANGE_PROXY',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +100,7 @@ export interface SwapQuoteConsumerOpts {
|
||||
*/
|
||||
export interface SwapQuoteGetOutputOpts {
|
||||
useExtensionContract: ExtensionContractType;
|
||||
extensionContractOpts?: ForwarderExtensionContractOpts | any;
|
||||
extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +115,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -121,6 +123,15 @@ export interface ForwarderExtensionContractOpts {
|
||||
feeRecipient: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isFromETH Whether the input token is ETH.
|
||||
* @param isToETH Whether the output token is ETH.
|
||||
*/
|
||||
export interface ExchangeProxyContractOpts {
|
||||
isFromETH: boolean;
|
||||
isToETH: boolean;
|
||||
}
|
||||
|
||||
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
|
||||
|
||||
export interface GetExtensionContractTypeOpts {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
|
||||
import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
|
||||
import {
|
||||
decodeFillQuoteTransformerData,
|
||||
decodePayTakerTransformerData,
|
||||
decodeWethTransformerData,
|
||||
ETH_TOKEN_ADDRESS,
|
||||
FillQuoteTransformerSide,
|
||||
} from '@0x/contracts-zero-ex';
|
||||
import { assetDataUtils } from '@0x/order-utils';
|
||||
import { Order } from '@0x/types';
|
||||
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
|
||||
import { constants } from '../src/constants';
|
||||
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
|
||||
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
|
||||
import { OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
const { NULL_ADDRESS } = constants;
|
||||
const { MAX_UINT256 } = contractConstants;
|
||||
|
||||
// tslint:disable: custom-no-magic-numbers
|
||||
|
||||
describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
const CHAIN_ID = 1;
|
||||
const TAKER_TOKEN = randomAddress();
|
||||
const MAKER_TOKEN = randomAddress();
|
||||
const contractAddresses = {
|
||||
...getContractAddressesForChainOrThrow(CHAIN_ID),
|
||||
exchangeProxy: randomAddress(),
|
||||
exchangeProxyAllowanceTarget: randomAddress(),
|
||||
transformers: {
|
||||
wethTransformer: randomAddress(),
|
||||
payTakerTransformer: randomAddress(),
|
||||
fillQuoteTransformer: randomAddress(),
|
||||
},
|
||||
};
|
||||
let consumer: ExchangeProxySwapQuoteConsumer;
|
||||
|
||||
before(async () => {
|
||||
const fakeProvider = {
|
||||
async sendAsync(): Promise<void> {
|
||||
/* noop */
|
||||
},
|
||||
};
|
||||
consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID });
|
||||
});
|
||||
|
||||
function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber {
|
||||
return getRandomInteger(1, maxAmount);
|
||||
}
|
||||
|
||||
function createAssetData(token?: string): string {
|
||||
return assetDataUtils.encodeERC20AssetData(token || randomAddress());
|
||||
}
|
||||
|
||||
function getRandomOrder(): OptimizedMarketOrder {
|
||||
return {
|
||||
fillableMakerAssetAmount: getRandomAmount(),
|
||||
fillableTakerFeeAmount: getRandomAmount(),
|
||||
fillableTakerAssetAmount: getRandomAmount(),
|
||||
fills: [],
|
||||
chainId: CHAIN_ID,
|
||||
exchangeAddress: contractAddresses.exchange,
|
||||
expirationTimeSeconds: getRandomInteger(1, 2e9),
|
||||
feeRecipientAddress: randomAddress(),
|
||||
makerAddress: randomAddress(),
|
||||
makerAssetAmount: getRandomAmount(),
|
||||
takerAssetAmount: getRandomAmount(),
|
||||
makerFee: getRandomAmount(),
|
||||
takerFee: getRandomAmount(),
|
||||
salt: getRandomAmount(2e9),
|
||||
signature: hexUtils.random(66),
|
||||
senderAddress: NULL_ADDRESS,
|
||||
takerAddress: NULL_ADDRESS,
|
||||
makerAssetData: createAssetData(MAKER_TOKEN),
|
||||
takerAssetData: createAssetData(TAKER_TOKEN),
|
||||
makerFeeAssetData: createAssetData(),
|
||||
takerFeeAssetData: createAssetData(),
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
|
||||
return {
|
||||
gasPrice: getRandomInteger(1, 1e9),
|
||||
type: side,
|
||||
makerAssetData: createAssetData(MAKER_TOKEN),
|
||||
takerAssetData: createAssetData(TAKER_TOKEN),
|
||||
orders: [getRandomOrder()],
|
||||
bestCaseQuoteInfo: {
|
||||
feeTakerAssetAmount: getRandomAmount(),
|
||||
makerAssetAmount: getRandomAmount(),
|
||||
gas: Math.floor(Math.random() * 8e6),
|
||||
protocolFeeInWeiAmount: getRandomAmount(),
|
||||
takerAssetAmount: getRandomAmount(),
|
||||
totalTakerAssetAmount: getRandomAmount(),
|
||||
},
|
||||
worstCaseQuoteInfo: {
|
||||
feeTakerAssetAmount: getRandomAmount(),
|
||||
makerAssetAmount: getRandomAmount(),
|
||||
gas: Math.floor(Math.random() * 8e6),
|
||||
protocolFeeInWeiAmount: getRandomAmount(),
|
||||
takerAssetAmount: getRandomAmount(),
|
||||
totalTakerAssetAmount: getRandomAmount(),
|
||||
},
|
||||
...(side === MarketOperation.Buy
|
||||
? { makerAssetFillAmount: getRandomAmount() }
|
||||
: { takerAssetFillAmount: getRandomAmount() }),
|
||||
} as any;
|
||||
}
|
||||
|
||||
function getRandomSellQuote(): MarketSellSwapQuote {
|
||||
return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote;
|
||||
}
|
||||
|
||||
function getRandomBuyQuote(): MarketBuySwapQuote {
|
||||
return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote;
|
||||
}
|
||||
|
||||
type PlainOrder = Exclude<Order, ['chainId', 'exchangeAddress']>;
|
||||
|
||||
function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] {
|
||||
return orders.map(
|
||||
o =>
|
||||
_.omit(o, [
|
||||
'chainId',
|
||||
'exchangeAddress',
|
||||
'fillableMakerAssetAmount',
|
||||
'fillableTakerAssetAmount',
|
||||
'fillableTakerFeeAmount',
|
||||
'fills',
|
||||
'signature',
|
||||
]) as PlainOrder,
|
||||
);
|
||||
}
|
||||
|
||||
const callDataEncoder = AbiEncoder.createMethod('transformERC20', [
|
||||
{ type: 'address', name: 'inputToken' },
|
||||
{ type: 'address', name: 'outputToken' },
|
||||
{ type: 'uint256', name: 'inputTokenAmount' },
|
||||
{ type: 'uint256', name: 'minOutputTokenAmount' },
|
||||
{
|
||||
type: 'tuple[]',
|
||||
name: 'transformations',
|
||||
components: [{ type: 'address', name: 'transformer' }, { type: 'bytes', name: 'data' }],
|
||||
},
|
||||
]);
|
||||
|
||||
interface CallArgs {
|
||||
inputToken: string;
|
||||
outputToken: string;
|
||||
inputTokenAmount: BigNumber;
|
||||
minOutputTokenAmount: BigNumber;
|
||||
transformations: Array<{
|
||||
transformer: string;
|
||||
data: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
describe('getCalldataOrThrow()', () => {
|
||||
it('can produce a sell quote', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
|
||||
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
|
||||
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
|
||||
expect(callArgs.transformations).to.be.length(2);
|
||||
expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
|
||||
expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
|
||||
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
|
||||
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
|
||||
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount);
|
||||
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
|
||||
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
|
||||
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
|
||||
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
|
||||
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
|
||||
expect(payTakerTransformerData.amounts).to.deep.eq([]);
|
||||
expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
|
||||
});
|
||||
|
||||
it('can produce a buy quote', async () => {
|
||||
const quote = getRandomBuyQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
|
||||
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
|
||||
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
|
||||
expect(callArgs.transformations).to.be.length(2);
|
||||
expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
|
||||
expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
|
||||
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
|
||||
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy);
|
||||
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount);
|
||||
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
|
||||
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
|
||||
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
|
||||
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
|
||||
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
|
||||
expect(payTakerTransformerData.amounts).to.deep.eq([]);
|
||||
expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
|
||||
});
|
||||
|
||||
it('ERC20 -> ERC20 does not have a WETH transformer', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
const transformers = callArgs.transformations.map(t => t.transformer);
|
||||
expect(transformers).to.not.include(contractAddresses.transformers.wethTransformer);
|
||||
});
|
||||
|
||||
it('ETH -> ERC20 has a WETH transformer before the fill', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
|
||||
extensionContractOpts: { isFromETH: true },
|
||||
});
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.transformations[0].transformer).to.eq(contractAddresses.transformers.wethTransformer);
|
||||
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data);
|
||||
expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS);
|
||||
});
|
||||
|
||||
it('ERC20 -> ETH has a WETH transformer after the fill', async () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
|
||||
extensionContractOpts: { isToETH: true },
|
||||
});
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.transformations[1].transformer).to.eq(contractAddresses.transformers.wethTransformer);
|
||||
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[1].data);
|
||||
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
|
||||
expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user