renamed asset-buyer + adding consumers

This commit is contained in:
David Sun 2019-06-06 12:52:13 -07:00
parent 64e3b6f5ee
commit 549bfe98f1
11 changed files with 326 additions and 123 deletions

View File

@ -11,29 +11,28 @@ import { constants } from './constants';
import { BasicOrderProvider } from './order_providers/basic_order_provider';
import { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
import {
AssetBuyerError,
AssetBuyerOpts,
BuyQuote,
BuyQuoteExecutionOpts,
BuyQuoteRequestOpts,
AssetSwapQuoterError,
AssetSwapQuoterOpts,
LiquidityForAssetData,
LiquidityRequestOpts,
OrderProvider,
OrdersAndFillableAmounts,
SwapQuote,
SwapQuoteRequestOpts,
} from './types';
import { assert } from './utils/assert';
import { assetDataUtils } from './utils/asset_data_utils';
import { buyQuoteCalculator } from './utils/buy_quote_calculator';
import { calculateLiquidity } from './utils/calculate_liquidity';
import { orderProviderResponseProcessor } from './utils/order_provider_response_processor';
import { swapQuoteCalculator } from './utils/swap_quote_calculator';
interface OrdersEntry {
ordersAndFillableAmounts: OrdersAndFillableAmounts;
lastRefreshTime: number;
}
export class AssetBuyer {
export class AssetSwapQuoter {
public readonly provider: ZeroExProvider;
public readonly orderProvider: OrderProvider;
public readonly networkId: number;
@ -44,7 +43,7 @@ export class AssetBuyer {
private readonly _ordersEntryMap: ObjectMap<OrdersEntry> = {};
/**
* Instantiates a new AssetBuyer instance given existing liquidity in the form of orders and feeOrders.
* @param supportedProvider The Provider instance you would like to use for interacting with the Ethereum network.
* @param supportedProvider The Provider instance you would like to use for ikknteracting with the Ethereum network.
* @param orders A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH).
* @param feeOrders A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array.
* @param options Initialization options for the AssetBuyer. See type definition for details.
@ -54,12 +53,12 @@ export class AssetBuyer {
public static getAssetBuyerForProvidedOrders(
supportedProvider: SupportedProvider,
orders: SignedOrder[],
options: Partial<AssetBuyerOpts> = {},
): AssetBuyer {
options: Partial<AssetSwapQuoterOpts> = {},
): AssetSwapQuoter {
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
assert.assert(orders.length !== 0, `Expected orders to contain at least one order`);
const orderProvider = new BasicOrderProvider(orders);
const assetBuyer = new AssetBuyer(supportedProvider, orderProvider, options);
const assetBuyer = new AssetSwapQuoter(supportedProvider, orderProvider, options);
return assetBuyer;
}
/**
@ -73,13 +72,13 @@ export class AssetBuyer {
public static getAssetBuyerForStandardRelayerAPIUrl(
supportedProvider: SupportedProvider,
sraApiUrl: string,
options: Partial<AssetBuyerOpts> = {},
): AssetBuyer {
options: Partial<AssetSwapQuoterOpts> = {},
): AssetSwapQuoter {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isWebUri('sraApiUrl', sraApiUrl);
const networkId = options.networkId || constants.DEFAULT_ASSET_BUYER_OPTS.networkId;
const networkId = options.networkId || constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS.networkId;
const orderProvider = new StandardRelayerAPIOrderProvider(sraApiUrl, networkId);
const assetBuyer = new AssetBuyer(provider, orderProvider, options);
const assetBuyer = new AssetSwapQuoter(provider, orderProvider, options);
return assetBuyer;
}
/**
@ -93,11 +92,11 @@ export class AssetBuyer {
constructor(
supportedProvider: SupportedProvider,
orderProvider: OrderProvider,
options: Partial<AssetBuyerOpts> = {},
options: Partial<AssetSwapQuoter> = {},
) {
const { networkId, orderRefreshIntervalMs, expiryBufferSeconds } = _.merge(
{},
constants.DEFAULT_ASSET_BUYER_OPTS,
constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS,
options,
);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
@ -115,23 +114,23 @@ export class AssetBuyer {
});
}
/**
* Get a `BuyQuote` containing all information relevant to fulfilling a buy given a desired assetData.
* You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy.
* Get a `SwapQuote` containing all information relevant to fulfilling a buy given a desired assetData.
* You can then pass the `SwapQuote` to `executeSwapQuoteAsync` to execute the buy.
* @param assetData The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* @param assetBuyAmount The amount of asset to buy.
* @param options Options for the request. See type definition for more information.
*
* @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
* @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information.
*/
public async getBuyQuoteAsync(
public async getSwapQuoteAsync(
makerAssetData: string,
takerAssetData: string,
makerAssetBuyAmount: BigNumber,
options: Partial<BuyQuoteRequestOpts> = {},
): Promise<BuyQuote> {
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<SwapQuote> {
const { shouldForceOrderRefresh, slippagePercentage } = _.merge(
{},
constants.DEFAULT_BUY_QUOTE_REQUEST_OPTS,
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData);
@ -151,39 +150,39 @@ export class AssetBuyer {
shouldForceOrderRefresh,
]);
if (ordersAndFillableAmounts.orders.length === 0) {
throw new Error(`${AssetBuyerError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`);
throw new Error(`${AssetSwapQuoterError.AssetUnavailable}: For makerAssetdata ${makerAssetData} and takerAssetdata ${takerAssetData}`);
}
const buyQuote = buyQuoteCalculator.calculate(
const swapQuote = swapQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
makerAssetBuyAmount,
slippagePercentage,
isMakerAssetZrxToken,
);
return buyQuote;
return swapQuote;
}
/**
* Get a `BuyQuote` containing all information relevant to fulfilling a buy given a desired ERC20 token address.
* You can then pass the `BuyQuote` to `executeBuyQuoteAsync` to execute the buy.
* Get a `SwapQuote` containing all information relevant to fulfilling a buy given a desired ERC20 token address.
* You can then pass the `SwapQuote` to `executeSwapQuoteAsync` to execute the buy.
* @param tokenAddress The ERC20 token address.
* @param assetBuyAmount The amount of asset to buy.
* @param options Options for the request. See type definition for more information.
*
* @return An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
* @return An object that conforms to SwapQuote that satisfies the request. See type definition for more information.
*/
public async getBuyQuoteForERC20TokenAddressAsync(
public async getSwapQuoteForERC20TokenAddressAsync(
makerTokenAddress: string,
takerTokenAddress: string,
makerAssetBuyAmount: BigNumber,
options: Partial<BuyQuoteRequestOpts> = {},
): Promise<BuyQuote> {
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<SwapQuote> {
assert.isETHAddressHex('makerTokenAddress', makerTokenAddress);
assert.isETHAddressHex('takerTokenAddress', takerTokenAddress);
assert.isBigNumber('makerAssetBuyAmount', makerAssetBuyAmount);
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const buyQuote = this.getBuyQuoteAsync(makerAssetData, takerAssetData, makerAssetBuyAmount, options);
return buyQuote;
const swapQuote = this.getSwapQuoteAsync(makerAssetData, takerAssetData, makerAssetBuyAmount, options);
return swapQuote;
}
/**
* Returns information about available liquidity for an asset
@ -224,13 +223,13 @@ export class AssetBuyer {
}
// /**
// * Given a BuyQuote and desired rate, attempt to execute the buy.
// * @param buyQuote An object that conforms to BuyQuote. See type definition for more information.
// * @param options Options for the execution of the BuyQuote. See type definition for more information.
// * Given a SwapQuote and desired rate, attempt to execute the buy.
// * @param SwapQuote An object that conforms to SwapQuote. See type definition for more information.
// * @param options Options for the execution of the SwapQuote. See type definition for more information.
// *
// * @return A promise of the txHash.
// */
// public async executeBuyQuoteAsync(
// public async executeSwapQuoteAsync(
// buyQuote: BuyQuote,
// options: Partial<BuyQuoteExecutionOpts> = {},
// ): Promise<string> {
@ -401,13 +400,6 @@ export class AssetBuyer {
return `${makerAssetData}_${takerAssetData}`;
}
/**
* Get the assetData that represents the WETH token.
* Will throw if WETH does not exist for the current network.
*/
// private _getEtherTokenAssetDataOrThrow(): string {
// return assetDataUtils.getEtherTokenAssetData(this._contractWrappers);
// }
/**
* Get the assetData that represents the ZRX token.
* Will throw if ZRX does not exist for the current network.

View File

@ -1,40 +1,35 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { AssetBuyerOpts, BuyQuoteExecutionOpts, BuyQuoteRequestOpts, OrdersAndFillableAmounts } from './types';
import { AssetSwapQuoterOpts, ForwarderSwapQuoteExecutionOpts, OrdersAndFillableAmounts, SwapQuoteRequestOpts, SwapQuoteExecutionOpts } from './types';
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const MAINNET_NETWORK_ID = 1;
const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = {
const DEFAULT_ASSET_SWAP_QUOTER_OPTS: AssetSwapQuoterOpts = {
networkId: MAINNET_NETWORK_ID,
orderRefreshIntervalMs: 10000, // 10 seconds
expiryBufferSeconds: 120, // 2 minutes
};
const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = {
feePercentage: 0,
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
shouldForceOrderRefresh: false,
slippagePercentage: 0.2, // 20% slippage protection
};
// Other default values are dynamically determined
const DEFAULT_BUY_QUOTE_EXECUTION_OPTS: BuyQuoteExecutionOpts = {
feeRecipient: NULL_ADDRESS,
slippagePercentage: 0.2, // 20% slippage protection,
allowMarketBuyOrders: true,
};
const EMPTY_ORDERS_AND_FILLABLE_AMOUNTS: OrdersAndFillableAmounts = {
orders: [] as SignedOrder[],
remainingFillableMakerAssetAmounts: [] as BigNumber[],
};
export const constants = {
ZERO_AMOUNT: new BigNumber(0),
NULL_ADDRESS,
MAINNET_NETWORK_ID,
ETHER_TOKEN_DECIMALS: 18,
DEFAULT_ASSET_BUYER_OPTS,
DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
DEFAULT_BUY_QUOTE_REQUEST_OPTS,
ONE_AMOUNT: new BigNumber(1),
DEFAULT_ASSET_SWAP_QUOTER_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
EMPTY_ORDERS_AND_FILLABLE_AMOUNTS,
};

View File

@ -1,6 +1,6 @@
import { BigNumber } from '@0x/utils';
import { AssetBuyerError } from './types';
import { AssetSwapQuoterError } from './types';
/**
* Error class representing insufficient asset liquidity
@ -14,7 +14,7 @@ export class InsufficientAssetLiquidityError extends Error {
* @param amountAvailableToFill The amount availabe to fill (in base units) factoring in slippage
*/
constructor(amountAvailableToFill: BigNumber) {
super(AssetBuyerError.InsufficientAssetLiquidity);
super(AssetSwapQuoterError.InsufficientAssetLiquidity);
this.amountAvailableToFill = amountAvailableToFill;
// Setting prototype so instanceof works. See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, InsufficientAssetLiquidityError.prototype);

View File

@ -18,19 +18,19 @@ export {
export { SignedOrder } from '@0x/types';
export { BigNumber } from '@0x/utils';
export { AssetBuyer } from './asset_buyer';
export { AssetSwapQuoter } from './asset_buyer';
export { InsufficientAssetLiquidityError } from './errors';
export { BasicOrderProvider } from './order_providers/basic_order_provider';
export { StandardRelayerAPIOrderProvider } from './order_providers/standard_relayer_api_order_provider';
export {
AssetBuyerError,
AssetBuyerOpts,
BuyQuote,
BuyQuoteExecutionOpts,
BuyQuoteInfo,
BuyQuoteRequestOpts,
AssetSwapQuoterError,
AssetSwapQuoterOpts,
SwapQuote,
SwapQuoteExecutionOpts,
SwapQuoteInfo,
SwapQuoteRequestOpts,
LiquidityForAssetData,
LiquidityRequestOpts,
OrdersAndFillableAmounts,

View File

@ -5,7 +5,7 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import {
AssetBuyerError,
AssetSwapQuoterError,
OrderProvider,
OrderProviderRequest,
OrderProviderResponse,
@ -73,7 +73,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider {
try {
orderbook = await this._sraClient.getOrderbookAsync(orderbookRequest, requestOpts);
} catch (err) {
throw new Error(AssetBuyerError.StandardRelayerApiError);
throw new Error(AssetSwapQuoterError.StandardRelayerApiError);
}
const apiOrders = orderbook.asks.records;
const orders = StandardRelayerAPIOrderProvider._getSignedOrderWithRemainingFillableMakerAssetAmountFromApi(
@ -101,7 +101,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider {
try {
response = await this._sraClient.getAssetPairsAsync(fullRequest);
} catch (err) {
throw new Error(AssetBuyerError.StandardRelayerApiError);
throw new Error(AssetSwapQuoterError.StandardRelayerApiError);
}
return _.map(response.records, item => {
if (item.assetDataA.assetData === takerAssetData) {
@ -129,7 +129,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider {
try {
response = await this._sraClient.getAssetPairsAsync(fullRequest);
} catch (err) {
throw new Error(AssetBuyerError.StandardRelayerApiError);
throw new Error(AssetSwapQuoterError.StandardRelayerApiError);
}
return _.map(response.records, item => {
if (item.assetDataA.assetData === makerAssetData) {

View File

@ -0,0 +1,172 @@
import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, Web3Wrapper, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
AssetSwapQuoterError,
CalldataInformation,
SmartContractParams,
SwapQuote,
SwapQuoteConsumer,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
SwapQuoteInfo,
Web3TransactionParams} from '../types';
import { assert } from '../utils/assert';
export interface ForwarderSwapQuoteGetOutputOpts extends SwapQuoteGetOutputOpts {
feePercentage: number;
feeRecipient: string;
}
export const FORWARDER_SWAP_QUOTE_CONSUMER_OPTS = {
feePercentage: 0,
feeRecipient: constants.NULL_ADDRESS,
};
const addAffiliateFeeToSwapQuoteInfo = (quoteInfo: SwapQuoteInfo, feePercentage: number): SwapQuoteInfo => {
const newQuoteInfo = _.clone(quoteInfo);
const affiliateFeeAmount = newQuoteInfo.takerTokenAmount.multipliedBy(feePercentage).integerValue(BigNumber.ROUND_CEIL);
const newFeeAmount = newQuoteInfo.feeTakerTokenAmount.plus(affiliateFeeAmount);
newQuoteInfo.feeTakerTokenAmount = newFeeAmount;
newQuoteInfo.totalTakerTokenAmount = newFeeAmount.plus(newQuoteInfo.takerTokenAmount);
return newQuoteInfo;
};
const addAffiliateFeeToSwapQuote = (quote: SwapQuote, feePercentage: number): SwapQuote => {
const newQuote = _.clone(quote);
newQuote.bestCaseQuoteInfo = addAffiliateFeeToSwapQuoteInfo(newQuote.bestCaseQuoteInfo, feePercentage);
newQuote.worstCaseQuoteInfo = addAffiliateFeeToSwapQuoteInfo(newQuote.worstCaseQuoteInfo, feePercentage);
return newQuote;
};
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumer {
public readonly provider: ZeroExProvider;
public readonly networkId: number;
private readonly _contractWrappers: ContractWrappers;
constructor(
supportedProvider: SupportedProvider,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { networkId } = _.merge(
{},
constants.DEFAULT_ASSET_SWAP_QUOTER_OPTS,
options,
);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isNumber('networkId', networkId);
this.provider = provider;
this.networkId = networkId;
this._contractWrappers = new ContractWrappers(this.provider, {
networkId,
});
}
public getCalldataOrThrowAsync = async (quote: SwapQuote, opts: Partial<ForwarderSwapQuoteGetOutputOpts>): Promise<CalldataInformation> => {
}
public getWeb3TransactionParamsOrThrowAsync = async (quote: SwapQuote, opts: Partial<ForwarderSwapQuoteGetOutputOpts>): Promise<Web3TransactionParams> => {
}
public getSmartContractParamsOrThrowAsync = async (quote: SwapQuote, opts: Partial<ForwarderSwapQuoteGetOutputOpts>): Promise<SmartContractParams> => {
const { feeRecipient, feePercentage } = _.merge(
{},
FORWARDER_SWAP_QUOTE_CONSUMER_OPTS,
opts,
);
assert.isValidSwapQuote('quote', quote);
assert.isNumber('feePercentage', feePercentage);
assert.isETHAddressHex('feeRecipient', feeRecipient);
const swapQuoteWithFeeAdded = addAffiliateFeeToSwapQuote(quote, feePercentage);
const { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = swapQuoteWithFeeAdded;
const params = {
orders: [],
makerAssetFillAmount: makerAssetBuyAmount,
signatures: [],
feeOrders: [],
feeSignatures: [],
feePercentage: [],
feeRecipient: [],
};
}
public executeSwapQuoteOrThrowAsync = async (quote: SwapQuote, opts: Partial<SwapQuoteExecutionOpts>): Promise<string> => {
const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice, feePercentage } = _.merge(
{},
FORWARDER_SWAP_QUOTE_CONSUMER_OPTS,
opts,
);
assert.isValidSwapQuote('quote', quote);
if (ethAmount !== undefined) {
assert.isBigNumber('ethAmount', ethAmount);
}
if (takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', takerAddress);
}
assert.isETHAddressHex('feeRecipient', feeRecipient);
if (gasLimit !== undefined) {
assert.isNumber('gasLimit', gasLimit);
}
if (gasPrice !== undefined) {
assert.isBigNumber('gasPrice', gasPrice);
}
const swapQuoteWithFeeAdded = addAffiliateFeeToSwapQuote(quote, feePercentage);
const { orders, feeOrders, makerAssetBuyAmount, worstCaseQuoteInfo } = swapQuoteWithFeeAdded;
let finalTakerAddress;
if (takerAddress !== undefined) {
finalTakerAddress = takerAddress;
} else {
const web3Wrapper = new Web3Wrapper(this.provider);
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
const firstAvailableAddress = _.head(availableAddresses);
if (firstAvailableAddress !== undefined) {
finalTakerAddress = firstAvailableAddress;
} else {
throw new Error(AssetSwapQuoterError.NoAddressAvailable);
}
}
try {
const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
orders,
makerAssetBuyAmount,
finalTakerAddress,
ethAmount || worstCaseQuoteInfo.totalTakerTokenAmount,
feeOrders,
feePercentage,
feeRecipient,
{
gasLimit,
gasPrice,
shouldValidate: true,
},
);
return txHash;
} catch (err) {
if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) {
throw new Error(AssetSwapQuoterError.SignatureRequestDenied);
} else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) {
throw new Error(AssetSwapQuoterError.TransactionValueTooLow);
} else {
throw err;
}
}
}
}

View File

@ -36,25 +36,70 @@ export interface OrderProvider {
getAvailableTakerAssetDatasAsync: (makerAssetData: string) => Promise<string[]>;
}
export interface CalldataInformation {
calldataHexString: string;
to: string;
value: BigNumber;
}
export interface Web3TransactionParams {
from: string;
to?: string;
value?: string;
gas?: string;
gasPrice?: string;
data?: string;
nonce?: string;
}
export interface SmartContractParams {
params: { [name: string]: any };
to: string;
value: BigNumber;
}
export interface SwapQuoteConsumer {
getCalldataOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteConsumerOpts>): Promise<CalldataInformation>;
getWeb3TransactionParamsOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteConsumerOpts>): Promise<Web3TransactionParams>;
getSmartContractParamsOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteConsumerOpts>): Promise<SmartContractParams>;
executeSwapQuoteOrThrowAsync(quote: SwapQuote, opts: Partial<SwapQuoteExecutionOpts>): Promise<string>;
}
export interface SwapQuoteConsumerOpts {
networkId: number;
}
export interface SwapQuoteGetOutputOpts {}
/**
* ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount.
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
* gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call.
* gasPrice: Gas price in Wei to use for a transaction
*/
export interface SwapQuoteExecutionOpts extends SwapQuoteConsumerOpts {
ethAmount?: BigNumber;
takerAddress?: string;
gasLimit?: number;
gasPrice?: BigNumber;
}
/**
* assetData: String that represents a specific asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* assetBuyAmount: The amount of asset to buy.
* orders: An array of objects conforming to SignedOrder. These orders can be used to cover the requested assetBuyAmount plus slippage.
* feeOrders: An array of objects conforming to SignedOrder. These orders can be used to cover the fees for the orders param above.
* feePercentage: Optional affiliate fee percentage used to calculate the eth amounts above.
* bestCaseQuoteInfo: Info about the best case price for the asset.
* worstCaseQuoteInfo: Info about the worst case price for the asset.
*/
export interface BuyQuote {
export interface SwapQuote {
takerAssetData: string;
makerAssetData: string;
makerAssetBuyAmount: BigNumber;
orders: SignedOrder[];
feeOrders: SignedOrder[];
bestCaseQuoteInfo: BuyQuoteInfo;
worstCaseQuoteInfo: BuyQuoteInfo;
toAddress: string; // exchange address, coordinator address
isUsingCoordinator: boolean;
bestCaseQuoteInfo: SwapQuoteInfo;
worstCaseQuoteInfo: SwapQuoteInfo;
}
/**
@ -62,7 +107,7 @@ export interface BuyQuote {
* feeEthAmount: The amount of eth required to pay the affiliate fee.
* totalEthAmount: The total amount of eth required to complete the buy (filling orders, feeOrders, and paying affiliate fee).
*/
export interface BuyQuoteInfo {
export interface SwapQuoteInfo {
takerTokenAmount: BigNumber;
feeTakerTokenAmount: BigNumber;
totalTakerTokenAmount: BigNumber;
@ -72,9 +117,10 @@ export interface BuyQuoteInfo {
* shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false.
* slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
*/
export interface BuyQuoteRequestOpts {
export interface SwapQuoteRequestOpts {
shouldForceOrderRefresh: boolean;
slippagePercentage: number;
allowMarketBuyOrders: boolean;
}
/*
@ -82,21 +128,10 @@ export interface BuyQuoteRequestOpts {
*
* shouldForceOrderRefresh: If set to true, new orders and state will be fetched instead of waiting for the next orderRefreshIntervalMs. Defaults to false.
*/
export type LiquidityRequestOpts = Pick<BuyQuoteRequestOpts, 'shouldForceOrderRefresh'>;
export type LiquidityRequestOpts = Pick<SwapQuoteRequestOpts, 'shouldForceOrderRefresh'>;
/**
* ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount.
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
* gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call.
* gasPrice: Gas price in Wei to use for a transaction
* feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
*/
export interface BuyQuoteExecutionOpts {
ethAmount?: BigNumber;
takerAddress?: string;
gasLimit?: number;
gasPrice?: BigNumber;
feeRecipient: string;
export interface ForwarderSwapQuoteExecutionOpts extends SwapQuoteExecutionOpts {
feeRecipient?: string;
}
/**
@ -104,7 +139,7 @@ export interface BuyQuoteExecutionOpts {
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).
* expiryBufferSeconds: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
*/
export interface AssetBuyerOpts {
export interface AssetSwapQuoterOpts {
networkId: number;
orderRefreshIntervalMs: number;
expiryBufferSeconds: number;
@ -113,7 +148,7 @@ export interface AssetBuyerOpts {
/**
* Possible error messages thrown by an AssetBuyer instance or associated static methods.
*/
export enum AssetBuyerError {
export enum AssetSwapQuoterError {
NoEtherTokenContractFound = 'NO_ETHER_TOKEN_CONTRACT_FOUND',
NoZrxTokenContractFound = 'NO_ZRX_TOKEN_CONTRACT_FOUND',
StandardRelayerApiError = 'STANDARD_RELAYER_API_ERROR',

View File

@ -1,29 +1,27 @@
import { assert as sharedAssert } from '@0x/assert';
import { schemas } from '@0x/json-schemas';
import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../types';
import { OrderProvider, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
export const assert = {
...sharedAssert,
isValidBuyQuote(variableName: string, buyQuote: BuyQuote): void {
sharedAssert.isHexString(`${variableName}.takerAssetData`, buyQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, buyQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, buyQuote.orders, schemas.signedOrdersSchema);
sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, buyQuote.feeOrders, schemas.signedOrdersSchema);
assert.isValidBuyQuoteInfo(`${variableName}.bestCaseQuoteInfo`, buyQuote.bestCaseQuoteInfo);
assert.isValidBuyQuoteInfo(`${variableName}.worstCaseQuoteInfo`, buyQuote.worstCaseQuoteInfo);
sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, buyQuote.makerAssetBuyAmount);
assert.isETHAddressHex(`${variableName}.toAddress`, buyQuote.toAddress);
assert.isBoolean(`${variableName}.isUsingCoordinator`, buyQuote.isUsingCoordinator);
isValidSwapQuote(variableName: string, swapQuote: SwapQuote): void {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
sharedAssert.doesConformToSchema(`${variableName}.feeOrders`, swapQuote.feeOrders, schemas.signedOrdersSchema);
assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo);
assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo);
sharedAssert.isBigNumber(`${variableName}.makerAssetBuyAmount`, swapQuote.makerAssetBuyAmount);
// TODO(dave4506) Remove once forwarder features are reimplemented
// if (buyQuote.feePercentage !== undefined) {
// sharedAssert.isNumber(`${variableName}.feePercentage`, buyQuote.feePercentage);
// }
},
isValidBuyQuoteInfo(variableName: string, buyQuoteInfo: BuyQuoteInfo): void {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, buyQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, buyQuoteInfo.feeTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, buyQuoteInfo.totalTakerTokenAmount);
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isBigNumber(`${variableName}.takerTokenAmount`, swapQuoteInfo.takerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.feeTakerTokenAmount`, swapQuoteInfo.feeTakerTokenAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerTokenAmount`, swapQuoteInfo.totalTakerTokenAmount);
},
isValidOrderProvider(variableName: string, orderFetcher: OrderProvider): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);

View File

@ -7,7 +7,7 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import {
AssetBuyerError,
AssetSwapQuoterError,
OrderProviderRequest,
OrderProviderResponse,
OrdersAndFillableAmounts,
@ -19,7 +19,7 @@ export const orderProviderResponseProcessor = {
const { makerAssetData, takerAssetData } = request;
_.forEach(response.orders, order => {
if (order.makerAssetData !== makerAssetData || order.takerAssetData !== takerAssetData) {
throw new Error(AssetBuyerError.InvalidOrderProviderResponse);
throw new Error(AssetSwapQuoterError.InvalidOrderProviderResponse);
}
});
},

View File

@ -4,17 +4,17 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors';
import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types';
import { AssetSwapQuoterError, OrdersAndFillableAmounts, SwapQuote, SwapQuoteInfo } from '../types';
// Calculates a buy quote for orders that have WETH as the takerAsset
export const buyQuoteCalculator = {
// Calculates a swap quote for orders
export const swapQuoteCalculator = {
calculate(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
makerAssetBuyAmount: BigNumber,
slippagePercentage: number,
isMakerAssetZrxToken: boolean,
): BuyQuote {
): SwapQuote {
const orders = ordersAndFillableAmounts.orders;
const remainingFillableMakerAssetAmounts = ordersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const feeOrders = feeOrdersAndFillableAmounts.orders;
@ -64,7 +64,7 @@ export const buyQuoteCalculator = {
);
// if we do not have enough feeOrders to cover the fees, throw
if (feeOrdersAndRemainingFeeAmount.remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
throw new Error(AssetSwapQuoterError.InsufficientZrxLiquidity);
}
resultFeeOrders = feeOrdersAndRemainingFeeAmount.resultFeeOrders;
feeOrdersRemainingFillableMakerAssetAmounts =
@ -106,9 +106,6 @@ export const buyQuoteCalculator = {
feeOrders: resultFeeOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
// TODO(dave4506): coordinator metadata for buy quote
toAddress: constants.NULL_ADDRESS,
isUsingCoordinator: false,
};
},
};
@ -118,7 +115,7 @@ function calculateQuoteInfo(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
makserAssetBuyAmount: BigNumber,
isMakerAssetZrxToken: boolean,
): BuyQuoteInfo {
): SwapQuoteInfo {
// find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right
let takerTokenAmount = constants.ZERO_AMOUNT;
let zrxTakerTokenAmount = constants.ZERO_AMOUNT;
@ -132,7 +129,9 @@ function calculateQuoteInfo(
// find eth amount needed to buy zrx
zrxTakerTokenAmount = findTakerTokenAmountNeededToBuyZrx(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
}
const feeTakerTokenAmount = zrxTakerTokenAmount;
// eth amount needed in total is the sum of the amount needed for the asset and the amount needed for fees
const totalTakerTokenAmount = takerTokenAmount.plus(feeTakerTokenAmount);
return {

View File

@ -0,0 +1,12 @@
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { constants } from '../constants';
export const utils = {
numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(
percentage,
);
},
};