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

asset-swapper: RFQ-T firm quotes #2541

Merged
merged 36 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3cdccb7
prep asset-swapper: rename variable
feuGeneA Mar 12, 2020
403fb38
asset-swapper: support firm RFQ-T quote requests
feuGeneA Mar 12, 2020
343caa1
migrations: Deploy ERC20BridgeSampler
feuGeneA Mar 26, 2020
0e1572c
migrations: add `yarn publish:private`
feuGeneA Apr 6, 2020
4b5a02c
asset-swapper, migrations: incl. prettier in lint
feuGeneA Mar 13, 2020
5602aac
mk SwapQuoterOpt rfqtTakerApiKeyWhitelist optional
feuGeneA Apr 9, 2020
1670b21
Use NULL_ADDRESS instead of literal
feuGeneA Apr 9, 2020
98fc780
Use map idiom instead of for loop
feuGeneA Apr 9, 2020
37390e0
Use Array.push instead of Array.concat
feuGeneA Apr 9, 2020
aadcf8f
Parallelize RFQ-T with orderbook query
feuGeneA Apr 9, 2020
63bfd23
Pass QuoteRequestor via SwapQuoteOpts not ctor arg
feuGeneA Apr 9, 2020
121d51b
Use try...catch instead of Promise.catch()
feuGeneA Apr 9, 2020
5f23833
rm unused SwapQuoteRequestOpts key enableRfqt
feuGeneA Apr 8, 2020
227676c
Remove unused intentOnFilling method parameter
feuGeneA Apr 9, 2020
93872ad
Push RFQ-T opts to own SwapQuoterOpts subnamespace
feuGeneA Apr 9, 2020
3c795d3
Demote instance member to just constructor a arg
feuGeneA Apr 9, 2020
fa617d2
Demote public member to private
feuGeneA Apr 9, 2020
70add44
Push RFQ-T opts to SwapQuoterReqOpts subnamespace
feuGeneA Apr 9, 2020
5f4778c
Don't throw when RFQ-T client isn't whitelisted
feuGeneA Apr 9, 2020
8cdc05f
Promote max maker response time to a global option
feuGeneA Apr 9, 2020
39c2a75
Promote a closure to a function
feuGeneA Apr 9, 2020
eb5ec58
Add `yarn prettier` script, & call it from `lint`
feuGeneA Apr 9, 2020
264407b
In Opts types, REQUIRE rfqt SUB-options
feuGeneA Apr 9, 2020
0cb5e45
Await Axios response so we don't circumvent catch
feuGeneA Apr 10, 2020
84adbcb
asset-swapper: Mockable axios for QuoteRequestor (#2549)
Apr 11, 2020
d55108a
Eliminate unnecessary `else`
feuGeneA Apr 11, 2020
ccc9e18
Type Axios response with undefined, not void
feuGeneA Apr 11, 2020
bb15f78
Validate maker endpoint responses with JSON Schema
feuGeneA Apr 11, 2020
27ca75d
Clarify parallelization of orderbook & RFQT
feuGeneA Apr 11, 2020
b854fcd
Remove an unnecessary type annotation
feuGeneA Apr 11, 2020
58d6256
Bug fix: RFQ-T orders werent going through sorting
feuGeneA Apr 11, 2020
47ef7ff
RFQ-T: validate assetData & add more tests (#2552)
Apr 15, 2020
aee758e
Fix bug: Stop ignoring default SwapQuoteRequestOps
feuGeneA Apr 14, 2020
3bdfcb8
Update {asset-s,migrat,contract-ad}* CHANGELOGs
feuGeneA Apr 15, 2020
513ddb4
Merge branch 'development' into rfq-t
feuGeneA Apr 15, 2020
1da8f68
migrations: Add independent `yarn prettier` script
feuGeneA Apr 15, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
feuGeneA marked this conversation as resolved.
Show resolved Hide resolved
"axios-mock-adapter": "^1.18.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

devDep?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because clients that use the exported mock need these deps.

"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;
feuGeneA marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you like lodash now, huh? 😆 If you're using it here, would prefer to use flatten as well 🤓

I'm ok leaving as it though ❤️


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`, {
steveklebanoff marked this conversation as resolved.
Show resolved Hide resolved
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
steveklebanoff marked this conversation as resolved.
Show resolved Hide resolved

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 => {
feuGeneA marked this conversation as resolved.
Show resolved Hide resolved
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