Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2541 from 0xProject/rfq-t
Browse files Browse the repository at this point in the history
asset-swapper: RFQ-T firm quotes
  • Loading branch information
feuGeneA committed Apr 15, 2020
2 parents 0e196a5 + 1da8f68 commit 110e1af
Show file tree
Hide file tree
Showing 15 changed files with 448 additions and 19 deletions.
4 changes: 4 additions & 0 deletions packages/asset-swapper/CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
{
"note": "Add DFB support + refactor swap quote calculator utils",
"pr": 2536
},
{
"note": "Add support for RFQ-T, querying maker-hosted endpoints for quotes to be submitted by the taker",
"pr": 2541
}
]
},
Expand Down
5 changes: 4 additions & 1 deletion packages/asset-swapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"build": "yarn tsc -b",
"watch": "tsc -w -p tsconfig.json",
"build:ci": "yarn build",
"lint": "tslint --format stylish --project .",
"lint": "tslint --format stylish --project . && yarn prettier",
"prettier": "prettier --check 'src/**/*.{ts,tsx,json,md}' --config ../../.prettierrc",
"fix": "tslint --fix --format stylish --project .",
"test": "yarn run_mocha",
"rebuild_and_test": "run-s clean build test",
Expand Down Expand Up @@ -53,6 +54,8 @@
"@0x/orderbook": "^2.2.5",
"@0x/utils": "^5.4.1",
"@0x/web3-wrapper": "^7.0.7",
"axios": "^0.19.2",
"axios-mock-adapter": "^1.18.1",
"heartbeats": "^5.0.1",
"lodash": "^4.17.11"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/asset-swapper/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ForwarderExtensionContractOpts,
OrderPrunerOpts,
OrderPrunerPermittedFeeTypes,
RfqtFirmQuoteRequestOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
SwapQuoteRequestOpts,
Expand Down Expand Up @@ -43,6 +44,10 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
},
...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 250e6,
rfqt: {
takerApiKeyWhitelist: [],
makerEndpoints: [],
},
};

const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
Expand All @@ -61,6 +66,10 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
};

const DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS: RfqtFirmQuoteRequestOpts = {
makerEndpointMaxResponseTimeMs: 1000,
};

export const constants = {
ETH_GAS_STATION_API_BASE_URL,
PROTOCOL_FEE_MULTIPLIER,
Expand All @@ -77,6 +86,7 @@ export const constants = {
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
DEFAULT_PER_PAGE,
DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS,
NULL_ERC20_ASSET_DATA,
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
Expand Down
5 changes: 5 additions & 0 deletions packages/asset-swapper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export {
GetExtensionContractTypeOpts,
LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
MockedRfqtFirmQuoteResponse,
RfqtFirmQuoteRequestOpts,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
Expand All @@ -64,3 +67,5 @@ export {
} from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { QuoteRequestor } from './utils/quote_requestor';
export { rfqtMocker } from './utils/rfqt_mocker';
61 changes: 47 additions & 14 deletions packages/asset-swapper/src/swap_quoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils';
import { ProtocolFeeUtils } from './utils/protocol_fee_utils';
import { QuoteRequestor } from './utils/quote_requestor';
import { sortingUtils } from './utils/sorting_utils';
import { SwapQuoteCalculator } from './utils/swap_quote_calculator';

Expand All @@ -42,6 +43,8 @@ export class SwapQuoter {
private readonly _devUtilsContract: DevUtilsContract;
private readonly _marketOperationUtils: MarketOperationUtils;
private readonly _orderStateUtils: OrderStateUtils;
private readonly _quoteRequestor: QuoteRequestor;
private readonly _rfqtTakerApiKeyWhitelist: string[];

/**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders.
Expand Down Expand Up @@ -161,10 +164,13 @@ export class SwapQuoter {
this.orderbook = orderbook;
this.expiryBufferMs = expiryBufferMs;
this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._rfqtTakerApiKeyWhitelist = options.rfqt ? options.rfqt.takerApiKeyWhitelist || [] : [];
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS);
this._orderStateUtils = new OrderStateUtils(this._devUtilsContract);
this._quoteRequestor =
options.quoteRequestor || new QuoteRequestor(options.rfqt ? options.rfqt.makerEndpoints || [] : []);
const sampler = new DexOrderSampler(
new IERC20BridgeSamplerContract(this._contractAddresses.erc20BridgeSampler, this.provider, {
gas: samplerGasLimit,
Expand Down Expand Up @@ -498,8 +504,7 @@ export class SwapQuoter {
this.permittedOrderFeeTypes,
this.expiryBufferMs,
);
const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders);
return sortedPrunedOrders;
return prunedOrders;
}

/**
Expand All @@ -512,40 +517,68 @@ export class SwapQuoter {
marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> {
const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
const opts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
let gasPrice: BigNumber;
if (!!options.gasPrice) {
gasPrice = options.gasPrice;
if (!!opts.gasPrice) {
gasPrice = opts.gasPrice;
assert.isBigNumber('gasPrice', gasPrice);
} else {
gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync();
}
// get the relevant orders for the makerAsset
let prunedOrders = await this._getSignedOrdersAsync(makerAssetData, takerAssetData);
// get batches of orders from different sources, awaiting sources in parallel
const orderBatchPromises: Array<Promise<SignedOrder[]>> = [];
orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book
if (
opts.rfqt &&
opts.rfqt.intentOnFilling &&
opts.apiKey &&
this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey)
) {
if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address');
}
orderBatchPromises.push(
this._quoteRequestor.requestRfqtFirmQuotesAsync(
makerAssetData,
takerAssetData,
assetFillAmount,
marketOperation,
opts.apiKey,
opts.rfqt.takerAddress,
),
);
}

const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises);

const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch));

const orders = sortingUtils.sortOrders(unsortedOrders);

// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling
if (prunedOrders.length === 0) {
prunedOrders = [
if (orders.length === 0) {
orders.push(
createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge),
];
);
}

let swapQuote: SwapQuote;

if (marketOperation === MarketOperation.Buy) {
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
prunedOrders,
orders,
assetFillAmount,
gasPrice,
calculateSwapQuoteOpts,
opts,
);
} else {
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
prunedOrders,
orders,
assetFillAmount,
gasPrice,
calculateSwapQuoteOpts,
opts,
);
}

Expand Down
28 changes: 28 additions & 0 deletions packages/asset-swapper/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';

import { GetMarketOrdersOpts } from './utils/market_operation_utils/types';
import { QuoteRequestor } from './utils/quote_requestor';

/**
* expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
Expand Down Expand Up @@ -192,6 +193,11 @@ export interface SwapQuoteOrdersBreakdown {
*/
export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts {
gasPrice?: BigNumber;
apiKey?: string;
rfqt?: {
takerAddress: string;
intentOnFilling: boolean;
};
}

/**
Expand All @@ -213,6 +219,11 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
contractAddresses?: ContractAddresses;
samplerGasLimit?: number;
liquidityProviderRegistryAddress?: string;
rfqt?: {
takerApiKeyWhitelist: string[];
makerEndpoints: string[];
};
quoteRequestor?: QuoteRequestor;
}

/**
Expand Down Expand Up @@ -262,3 +273,20 @@ export enum OrderPrunerPermittedFeeTypes {
MakerDenominatedTakerFee = 'MAKER_DENOMINATED_TAKER_FEE',
TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE',
}

export interface RfqtFirmQuoteRequestOpts {
makerEndpointMaxResponseTimeMs?: number;
}

/**
* Represents a mocked RFQT maker responses.
*/
export interface MockedRfqtFirmQuoteResponse {
endpoint: string;
requestApiKey: string;
requestParams: {
[key: string]: string | undefined;
};
responseData: any;
responseCode: number;
}
116 changes: 116 additions & 0 deletions packages/asset-swapper/src/utils/quote_requestor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { schemas, SchemaValidator } from '@0x/json-schemas';
import { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { ERC20AssetData } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import Axios, { AxiosResponse } from 'axios';
import * as _ from 'lodash';

import { constants } from '../constants';
import { MarketOperation, RfqtFirmQuoteRequestOpts } from '../types';

/**
* Request quotes from RFQ-T providers
*/

function getTokenAddressOrThrow(assetData: string): string {
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (decodedAssetData.hasOwnProperty('tokenAddress')) {
// type cast necessary here as decodeAssetDataOrThrow returns
// an AssetData object, which doesn't necessarily contain a
// token address. (it could possibly be a StaticCallAssetData,
// which lacks an address.) so we'll just assume it's a token
// here. should be safe, with the enclosing guard condition
// and subsequent error.
// tslint:disable-next-line:no-unnecessary-type-assertion
return (decodedAssetData as ERC20AssetData).tokenAddress;
}
throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`);
}

export class QuoteRequestor {
private readonly _rfqtMakerEndpoints: string[];
private readonly _schemaValidator: SchemaValidator = new SchemaValidator();

constructor(rfqtMakerEndpoints: string[]) {
this._rfqtMakerEndpoints = rfqtMakerEndpoints;
}

public async requestRfqtFirmQuotesAsync(
makerAssetData: string,
takerAssetData: string,
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
takerApiKey: string,
takerAddress: string,
options?: Partial<RfqtFirmQuoteRequestOpts>,
): Promise<SignedOrder[]> {
const { makerEndpointMaxResponseTimeMs } = _.merge({}, constants.DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS, options);

const buyToken = getTokenAddressOrThrow(makerAssetData);
const sellToken = getTokenAddressOrThrow(takerAssetData);

// create an array of promises for quote responses, using "undefined"
// as a placeholder for failed requests.
const responsesIfDefined: Array<undefined | AxiosResponse<SignedOrder>> = await Promise.all(
this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => {
try {
return await Axios.get<SignedOrder>(`${rfqtMakerEndpoint}/quote`, {
headers: { '0x-api-key': takerApiKey },
params: {
sellToken,
buyToken,
buyAmount: marketOperation === MarketOperation.Buy ? assetFillAmount.toString() : undefined,
sellAmount:
marketOperation === MarketOperation.Sell ? assetFillAmount.toString() : undefined,
takerAddress,
},
timeout: makerEndpointMaxResponseTimeMs,
});
} catch (err) {
logUtils.warn(
`Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${takerApiKey} for taker address ${takerAddress}`,
);
logUtils.warn(err);
return undefined;
}
}),
);

const responses = responsesIfDefined.filter(
(respIfDefd): respIfDefd is AxiosResponse<SignedOrder> => respIfDefd !== undefined,
);

const ordersWithStringInts = responses.map(response => response.data); // not yet BigNumber

const validatedOrdersWithStringInts = ordersWithStringInts.filter(order => {
const hasValidSchema = this._schemaValidator.isValid(order, schemas.signedOrderSchema);
if (!hasValidSchema) {
logUtils.warn(`Invalid RFQ-t order received, filtering out: ${JSON.stringify(order)}`);
return false;
}

const hasExpectedMakerAssetData = order.makerAssetData.toLowerCase() === makerAssetData.toLowerCase();
const hasExpectedTakerAssetData = order.takerAssetData.toLowerCase() === takerAssetData.toLowerCase();
if (!hasExpectedMakerAssetData || !hasExpectedTakerAssetData) {
logUtils.warn(`Unexpected asset data in RFQ-T order, filtering out: ${JSON.stringify(order)}`);
return false;
}

return true;
});

const orders: SignedOrder[] = validatedOrdersWithStringInts.map(orderWithStringInts => {
return {
...orderWithStringInts,
makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount),
takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount),
makerFee: new BigNumber(orderWithStringInts.makerFee),
takerFee: new BigNumber(orderWithStringInts.takerFee),
expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds),
salt: new BigNumber(orderWithStringInts.salt),
};
});

return orders;
}
}
Loading

0 comments on commit 110e1af

Please sign in to comment.