add forwarder logic for market sell

This commit is contained in:
David Sun 2019-06-28 13:32:51 -07:00
parent 288a7d4cea
commit 64a0080616
7 changed files with 209 additions and 67 deletions

View File

@ -8,8 +8,11 @@ import { constants } from '../constants';
import {
CalldataInfo,
ForwarderMarketBuySmartContractParams,
ForwarderMarketSellSmartContractParams,
ForwarderSwapQuoteExecutionOpts,
ForwarderSwapQuoteGetOutputOpts,
MarketBuySwapQuote,
MarketSellSwapQuote,
SmartContractParamsInfo,
SwapQuote,
SwapQuoteConsumer,
@ -22,7 +25,8 @@ import { assetDataUtils } from '../utils/asset_data_utils';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
import { utils } from '../utils/utils';
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer<ForwarderMarketBuySmartContractParams> {
export class ForwarderSwapQuoteConsumer
implements SwapQuoteConsumer<ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams> {
public readonly provider: ZeroExProvider;
public readonly networkId: number;
@ -51,17 +55,35 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer<ForwarderMa
): Promise<CalldataInfo> {
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { params, to, ethAmount, methodAbi } = await this.getSmartContractParamsOrThrowAsync(quote, opts);
const consumableQuote = (quote as any) as (MarketBuySwapQuote | MarketSellSwapQuote);
const smartContractParamsInfo = await this.getSmartContractParamsOrThrowAsync(consumableQuote, opts);
const { to, methodAbi, ethAmount } = smartContractParamsInfo;
const abiEncoder = new AbiEncoder.Method(methodAbi);
const args = [
params.orders,
params.makerAssetFillAmount,
params.signatures,
params.feeOrders,
params.feeSignatures,
params.feePercentage,
params.feeRecipient,
];
let args: any[];
if (utils.isSwapQuoteMarketBuy(consumableQuote)) {
const marketBuyParams = (smartContractParamsInfo.params as any) as ForwarderMarketBuySmartContractParams;
args = [
marketBuyParams.orders,
marketBuyParams.makerAssetFillAmount,
marketBuyParams.signatures,
marketBuyParams.feeOrders,
marketBuyParams.feeSignatures,
marketBuyParams.feePercentage,
marketBuyParams.feeRecipient,
];
} else {
const marketSellParams = (smartContractParamsInfo.params as any) as ForwarderMarketSellSmartContractParams;
args = [
marketSellParams.orders,
marketSellParams.signatures,
marketSellParams.feeOrders,
marketSellParams.feeSignatures,
marketSellParams.feePercentage,
marketSellParams.feeRecipient,
];
}
const calldataHexString = abiEncoder.encode(args);
return {
calldataHexString,
@ -79,7 +101,9 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer<ForwarderMa
public async getSmartContractParamsOrThrowAsync(
quote: SwapQuote,
opts: Partial<ForwarderSwapQuoteGetOutputOpts>,
): Promise<SmartContractParamsInfo<ForwarderMarketBuySmartContractParams>> {
): Promise<
SmartContractParamsInfo<ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams>
> {
assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow());
const { ethAmount, feeRecipient, feePercentage: unFormattedFeePercentage } = _.merge(
@ -99,26 +123,51 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer<ForwarderMa
unFormattedFeePercentage,
);
const { orders, feeOrders, makerAssetFillAmount, worstCaseQuoteInfo } = swapQuoteWithAffiliateFee;
const consumableQuoteWithAffiliateFee = (swapQuoteWithAffiliateFee as any) as (
| MarketBuySwapQuote
| MarketSellSwapQuote);
const { orders, feeOrders, worstCaseQuoteInfo } = swapQuoteWithAffiliateFee;
const signatures = _.map(orders, o => o.signature);
const feeSignatures = _.map(feeOrders, o => o.signature);
const feePercentage = utils.numberPercentageToEtherTokenAmountPercentage(unFormattedFeePercentage);
const params: ForwarderMarketBuySmartContractParams = {
orders,
makerAssetFillAmount,
signatures,
feeOrders,
feeSignatures,
feePercentage,
feeRecipient,
};
let params: ForwarderMarketBuySmartContractParams | ForwarderMarketSellSmartContractParams;
let methodName: string;
if (utils.isSwapQuoteMarketBuy(consumableQuoteWithAffiliateFee)) {
const { makerAssetFillAmount } = consumableQuoteWithAffiliateFee;
params = {
orders,
makerAssetFillAmount,
signatures,
feeOrders,
feeSignatures,
feePercentage,
feeRecipient,
};
methodName = 'marketBuyOrdersWithEth';
} else {
const { takerAssetFillAmount } = consumableQuoteWithAffiliateFee;
params = {
orders,
takerAssetFillAmount,
signatures,
feeOrders,
feeSignatures,
feePercentage,
feeRecipient,
};
methodName = 'marketSellOrdersWithEth';
}
const methodAbi = utils.getMethodAbiFromContractAbi(
this._contractWrappers.forwarder.abi,
'marketBuyOrdersWithEth',
methodName,
) as MethodAbi;
return {
@ -163,25 +212,47 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer<ForwarderMa
const swapQuoteWithAffiliateFee = affiliateFeeUtils.getSwapQuoteWithAffiliateFee(quote, feePercentage);
const { orders, feeOrders, makerAssetFillAmount, worstCaseQuoteInfo } = swapQuoteWithAffiliateFee;
const consumableQuoteWithAffiliateFee = (swapQuoteWithAffiliateFee as any) as (
| MarketBuySwapQuote
| MarketSellSwapQuote);
const { orders, feeOrders, worstCaseQuoteInfo } = consumableQuoteWithAffiliateFee;
const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts);
try {
const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
orders,
makerAssetFillAmount,
finalTakerAddress,
ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount,
feeOrders,
feePercentage,
feeRecipient,
{
gasLimit,
gasPrice,
shouldValidate: true,
},
);
let txHash: string;
if (utils.isSwapQuoteMarketBuy(consumableQuoteWithAffiliateFee)) {
const { makerAssetFillAmount } = consumableQuoteWithAffiliateFee;
txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
orders,
makerAssetFillAmount,
finalTakerAddress,
ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount,
feeOrders,
feePercentage,
feeRecipient,
{
gasLimit,
gasPrice,
shouldValidate: true,
},
);
} else {
txHash = await this._contractWrappers.forwarder.marketSellOrdersWithEthAsync(
orders,
finalTakerAddress,
ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount,
feeOrders,
feePercentage,
feeRecipient,
{
gasLimit,
gasPrice,
shouldValidate: true,
},
);
}
return txHash;
} catch (err) {
if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) {

View File

@ -167,12 +167,12 @@ export class SwapQuoter {
);
}
const swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
takerAssetSellAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
takerAssetSellAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
return swapQuote;
}
@ -221,12 +221,12 @@ export class SwapQuoter {
);
}
const swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
makerAssetBuyAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
makerAssetBuyAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
return swapQuote;
}
@ -276,7 +276,12 @@ export class SwapQuoter {
assert.isBigNumber('takerAssetSellAmount', takerAssetSellAmount);
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const swapQuote = this.getMarketSellSwapQuoteAsync(makerAssetData, takerAssetData, takerAssetSellAmount, options);
const swapQuote = this.getMarketSellSwapQuoteAsync(
makerAssetData,
takerAssetData,
takerAssetSellAmount,
options,
);
return swapQuote;
}
/**

View File

@ -94,12 +94,25 @@ export interface ForwarderMarketBuySmartContractParams extends ExchangeMarketBuy
feeRecipient: string;
}
export interface ForwarderMarketSellSmartContractParams extends ExchangeMarketSellSmartContractParams {
feeOrders: SignedOrder[];
feeSignatures: string[];
feePercentage: BigNumber;
feeRecipient: string;
}
export interface ExchangeMarketBuySmartContractParams {
orders: SignedOrder[];
makerAssetFillAmount: BigNumber;
signatures: string[];
}
export interface ExchangeMarketSellSmartContractParams {
orders: SignedOrder[];
takerAssetFillAmount: BigNumber;
signatures: string[];
}
/**
* Interface that varying SwapQuoteConsumers adhere to (exchange consumer, router consumer, forwarder consumer, coordinator consumer)
* getCalldataOrThrow: Get CalldataInfo to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided.
@ -236,6 +249,7 @@ export interface SwapQuoterOpts {
* Possible error messages thrown by an SwapQuoterConsumer instance or associated static methods.
*/
export enum SwapQuoteConsumerError {
InvalidMarketSellOrMarketBuySwapQuote = 'INVALID_MARKET_BUY_SELL_SWAP_QUOTE',
InvalidForwarderSwapQuote = 'INVALID_FORWARDER_SWAP_QUOTE_PROVIDED',
NoAddressAvailable = 'NO_ADDRESS_AVAILABLE',
SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED',

View File

@ -3,7 +3,8 @@ import { schemas } from '@0x/json-schemas';
import { SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteConsumerError, SwapQuoteInfo } from '../types';
import { utils } from '../utils/utils';
export const assert = {
...sharedAssert,
@ -14,7 +15,13 @@ export const assert = {
sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema);
assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo);
assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo);
sharedAssert.isBigNumber(`${variableName}.makerAssetFillAmount`, swapQuote.makerAssetFillAmount);
if (utils.isSwapQuoteMarketBuy(swapQuote)) {
sharedAssert.isBigNumber(`${variableName}.makerAssetFillAmount`, swapQuote.makerAssetFillAmount);
} else if (utils.isSwapQuoteMarketSell(swapQuote)) {
sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount);
} else {
throw new Error(SwapQuoteConsumerError.InvalidMarketSellOrMarketBuySwapQuote);
}
},
isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void {
assert.isValidSwapQuote(variableName, swapQuote);
@ -33,9 +40,15 @@ export const assert = {
);
},
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, swapQuoteInfo.feeTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, swapQuoteInfo.totalTakerTokenAmount);
if (utils.isSwapQuoteInfoMarketBuy(swapQuoteInfo)) {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount);
} else if (utils.isSwapQuoteInfoMarketSell(swapQuoteInfo)) {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.makerTokenAmount);
} else {
throw new Error(SwapQuoteConsumerError.InvalidMarketSellOrMarketBuySwapQuote);
}
},
isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);

View File

@ -4,7 +4,14 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors';
import { MarketBuySwapQuote, MarketBuySwapQuoteInfo, MarketSellSwapQuote, MarketSellSwapQuoteInfo, OrdersAndFillableAmounts, SwapQuoterError } from '../types';
import {
MarketBuySwapQuote,
MarketBuySwapQuoteInfo,
MarketSellSwapQuote,
MarketSellSwapQuoteInfo,
OrdersAndFillableAmounts,
SwapQuoterError,
} from '../types';
// Calculates a swap quote for orders
export const swapQuoteCalculator = {
@ -17,9 +24,11 @@ export const swapQuoteCalculator = {
): MarketSellSwapQuote {
const orders = ordersAndFillableAmounts.orders;
const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const remainingFillableTakerAssetAmounts = remainingFillableMakerAssetAmounts.map((makerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getTakerFillAmount(orders[index], makerAssetAmount);
});
const remainingFillableTakerAssetAmounts = remainingFillableMakerAssetAmounts.map(
(makerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getTakerFillAmount(orders[index], makerAssetAmount);
},
);
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = takerAssetFillAmount.multipliedBy(slippagePercentage).integerValue();
@ -32,9 +41,12 @@ export const swapQuoteCalculator = {
remainingFillableTakerAssetAmounts,
slippageBufferAmount,
});
const ordersRemainingFillableMakerAssetAmounts = _.map(ordersRemainingFillableTakerAssetAmounts, (takerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getMakerFillAmount(resultOrders[index], takerAssetAmount);
});
const ordersRemainingFillableMakerAssetAmounts = _.map(
ordersRemainingFillableTakerAssetAmounts,
(takerAssetAmount: BigNumber, index: number) => {
return orderCalculationUtils.getMakerFillAmount(resultOrders[index], takerAssetAmount);
},
);
// if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
// We needed the amount they requested to buy, plus the amount for slippage
@ -260,7 +272,10 @@ function calculateMarketSellQuoteInfo(
let makerTokenAmount = constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
if (isMakerAssetZrxToken) {
makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount(ordersAndFillableAmounts, takerAssetSellAmount);
makerTokenAmount = findZrxTokenAmountFromSellingTakerTokenAmount(
ordersAndFillableAmounts,
takerAssetSellAmount,
);
} else {
// find eth and zrx amounts needed to buy
const takerTokenAndZrxAmountToBuyAsset = findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset(
@ -304,7 +319,10 @@ function findZrxTokenAmountFromSellingTakerTokenAmount(
(acc, order, index) => {
const { totalZrxTokenAmount, remainingTakerAssetFillAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount(order, remainingFillableMakerAssetAmount);
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount(
order,
remainingFillableMakerAssetAmount,
);
const takerFillAmount = BigNumber.min(remainingTakerAssetFillAmount, remainingFillableTakerAssetAmount);
const makerFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerFillAmount);
const feeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
@ -400,7 +418,10 @@ function findMakerTokenAmountReceivedAndZrxAmountNeededToSellAsset(
(acc, order, index) => {
const { totalMakerTokenAmount, totalZrxAmount, remainingTakerAssetFillAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const remainingFillableTakerAssetAmount = orderCalculationUtils.getTakerFillAmount(order, remainingFillableMakerAssetAmount);
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);

View File

@ -4,6 +4,14 @@ import { AbiDefinition, ContractAbi, MethodAbi } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
MarketBuySwapQuote,
MarketBuySwapQuoteInfo,
MarketSellSwapQuote,
MarketSellSwapQuoteInfo,
SwapQuote,
SwapQuoteInfo,
} from '../types';
// tslint:disable:no-unnecessary-type-assertion
export const utils = {
@ -25,4 +33,16 @@ export const utils = {
},
) as MethodAbi | undefined;
},
isSwapQuoteMarketBuy(quote: SwapQuote): quote is MarketBuySwapQuote {
return (quote as MarketSellSwapQuote).takerAssetFillAmount !== undefined;
},
isSwapQuoteMarketSell(quote: SwapQuote): quote is MarketSellSwapQuote {
return (quote as MarketBuySwapQuote).makerAssetFillAmount !== undefined;
},
isSwapQuoteInfoMarketBuy(quote: SwapQuoteInfo): quote is MarketBuySwapQuoteInfo {
return (quote as MarketBuySwapQuoteInfo).takerTokenAmount !== undefined;
},
isSwapQuoteInfoMarketSell(quote: SwapQuoteInfo): quote is MarketSellSwapQuoteInfo {
return (quote as MarketSellSwapQuoteInfo).makerTokenAmount !== undefined;
},
};

View File

@ -5,9 +5,7 @@ import * as _ from 'lodash';
import { assert } from './assert';
import { constants } from './constants';
import {
orderCalculationUtils,
} from './order_calculation_utils';
import { orderCalculationUtils } from './order_calculation_utils';
import {
FeeOrdersAndRemainingFeeAmount,
FindFeeOrdersThatCoverFeesForTargetOrdersOpts,