asset-swapper: RFQ-T indicative quotes

These changes have been exercised via mocha tests in the 0x-api repo.

Not sure why I had to add GetMarketOrdersRfqtOpts to the package
exports.  `yarn test:generate_docs:circleci` said:

$ node ./packages/monorepo-scripts/lib/doc_generate.js --package @0x/asset-swapper
GENERATE_DOCS: Generating Typedoc JSON for @0x/asset-swapper...
GENERATE_DOCS: Generating Typedoc Markdown for @0x/asset-swapper...
GENERATE_DOCS: Modifying Markdown To Exclude Unexported Items...
Error: @0x/asset-swapper package needs to export:
GetMarketOrdersRfqtOpts
From it's index.ts. If any are from external dependencies, then add them to the EXTERNAL_TYPE_MAP.
    at DocGenerateUtils._lookForMissingReferenceExportsThrowIfExists (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:288:19)
    at DocGenerateUtils.<anonymous> (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:255:34)
    at step (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:32:23)
    at Object.next (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:13:53)
    at fulfilled (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:4:58)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:189:7)
This commit is contained in:
F. Eugene Aumson
2020-04-15 18:19:36 -04:00
parent 33fdfdc8c0
commit d6d4d29257
9 changed files with 222 additions and 20 deletions

View File

@@ -64,8 +64,9 @@ export {
CollapsedFill,
NativeCollapsedFill,
OptimizedMarketOrder,
GetMarketOrdersRfqtOpts,
} 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 { QuoteRequestor, RfqtIndicativeQuoteResponse } from './utils/quote_requestor';
export { rfqtMocker } from './utils/rfqt_mocker';

View File

@@ -9,6 +9,7 @@ import * as _ from 'lodash';
import { constants } from './constants';
import {
CalculateSwapQuoteOpts,
LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote,
MarketOperation,
@@ -24,7 +25,6 @@ import { calculateLiquidity } from './utils/calculate_liquidity';
import { MarketOperationUtils } from './utils/market_operation_utils';
import { createDummyOrderForSampler } from './utils/market_operation_utils/orders';
import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { GetMarketOrdersOpts } from './utils/market_operation_utils/types';
import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils';
import { ProtocolFeeUtils } from './utils/protocol_fee_utils';
@@ -565,19 +565,30 @@ export class SwapQuoter {
let swapQuote: SwapQuote;
const calcOpts: CalculateSwapQuoteOpts = opts;
if (
// we should request indicative quotes:
calcOpts.rfqt &&
!calcOpts.rfqt.intentOnFilling &&
calcOpts.rfqt.apiKey &&
this._rfqtTakerApiKeyWhitelist.includes(calcOpts.rfqt.apiKey)
) {
calcOpts.rfqt.quoteRequestor = this._quoteRequestor;
}
if (marketOperation === MarketOperation.Buy) {
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
orders,
assetFillAmount,
gasPrice,
opts,
calcOpts,
);
} else {
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
orders,
assetFillAmount,
gasPrice,
opts,
calcOpts,
);
}

View File

@@ -287,3 +287,16 @@ export interface MockedRfqtFirmQuoteResponse {
responseData: any;
responseCode: number;
}
/**
* Represents a mocked RFQT maker responses.
*/
export interface MockedRfqtIndicativeQuoteResponse {
endpoint: string;
requestApiKey: string;
requestParams: {
[key: string]: string | undefined;
};
responseData: any;
responseCode: number;
}

View File

@@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import { MarketOperation } from '../../types';
import { RfqtIndicativeQuoteResponse } from '../quote_requestor';
import { difference } from '../utils';
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
@@ -13,7 +14,12 @@ import {
getPathAdjustedSlippage,
getPathSize,
} from './fills';
import { createOrdersFromPath, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders';
import {
createOrdersFromPath,
createSignedOrdersFromRfqtIndicativeQuotes,
createSignedOrdersWithFillableAmounts,
getNativeOrderTokens,
} from './orders';
import { findOptimalPath } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler';
import {
@@ -57,12 +63,7 @@ export class MarketOperationUtils {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
// Call the sampler contract.
const [
orderFillableAmounts,
liquidityProviderAddress,
ethToMakerAssetRate,
dexQuotes,
] = await this._sampler.executeAsync(
const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
@@ -92,10 +93,25 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry,
),
);
const rfqtPromise =
_opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined
? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
takerAmount,
MarketOperation.Sell,
_opts.rfqt,
)
: Promise.resolve<RfqtIndicativeQuoteResponse[]>([]);
const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes],
rfqtIndicativeQuotes,
] = await Promise.all([samplerPromise, rfqtPromise]);
return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders,
dexQuotes,
rfqtIndicativeQuotes,
liquidityProviderAddress,
inputToken: takerToken,
outputToken: makerToken,
@@ -130,12 +146,7 @@ export class MarketOperationUtils {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
// Call the sampler contract.
const [
orderFillableAmounts,
liquidityProviderAddress,
ethToTakerAssetRate,
dexQuotes,
] = await this._sampler.executeAsync(
const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
@@ -165,11 +176,26 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry,
),
);
const rfqtPromise =
opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined
? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
makerAmount,
MarketOperation.Buy,
_opts.rfqt,
)
: [];
const [
[orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes],
rfqtIndicativeQuotes,
] = await Promise.all([samplerPromise, rfqtPromise]);
return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders,
dexQuotes,
rfqtIndicativeQuotes,
liquidityProviderAddress,
inputToken: makerToken,
outputToken: takerToken,
@@ -246,6 +272,7 @@ export class MarketOperationUtils {
orderFillableAmounts,
nativeOrders,
dexQuotes,
rfqtIndicativeQuotes: [],
inputToken: makerToken,
outputToken: takerToken,
side: MarketOperation.Buy,
@@ -274,6 +301,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[];
orderFillableAmounts: BigNumber[];
dexQuotes: DexSample[][];
rfqtIndicativeQuotes: RfqtIndicativeQuoteResponse[];
runLimit?: number;
ethToOutputRate?: BigNumber;
bridgeSlippage?: number;
@@ -290,7 +318,10 @@ export class MarketOperationUtils {
const paths = createFillPaths({
side,
// Augment native orders with their fillable amounts.
orders: createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts),
orders: [
...createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts),
...createSignedOrdersFromRfqtIndicativeQuotes(opts.rfqtIndicativeQuotes),
],
dexQuotes: opts.dexQuotes,
targetInput: inputAmount,
ethToOutputRate: opts.ethToOutputRate,

View File

@@ -4,6 +4,7 @@ import { ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { RfqtIndicativeQuoteResponse } from '../quote_requestor';
import {
DEFAULT_CURVE_OPTS,
@@ -358,3 +359,32 @@ function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
...(fill as NativeCollapsedFill).nativeOrder,
};
}
export function createSignedOrdersFromRfqtIndicativeQuotes(
quotes: RfqtIndicativeQuoteResponse[],
): SignedOrderWithFillableAmounts[] {
return quotes.map(quote => {
return {
fillableMakerAssetAmount: quote.makerAssetAmount,
fillableTakerAssetAmount: quote.takerAssetAmount,
makerAssetAmount: quote.makerAssetAmount,
takerAssetAmount: quote.takerAssetAmount,
makerAssetData: quote.makerAssetData,
takerAssetData: quote.takerAssetData,
takerAddress: NULL_ADDRESS,
makerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT, // generatePseudoRandomSalt(),
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
chainId: 0, // HACK !!!!!!!!! how can we get at this from this context?
exchangeAddress: NULL_ADDRESS, // HACK !!!!!!!!! how can we get at this from this context?
};
});
}

View File

@@ -1,7 +1,8 @@
import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { BigNumber } from '@0x/utils';
import { SignedOrderWithFillableAmounts } from '../../types';
import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { QuoteRequestor, RfqtIndicativeQuoteResponse } from '../../utils/quote_requestor';
/**
* Order domain keys: chainId and exchange
@@ -34,6 +35,7 @@ export enum ERC20BridgeSource {
CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD',
LiquidityProvider = 'LiquidityProvider',
CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD',
Rfqt = 'Rfqt',
}
// Internal `fillData` field for `Fill` objects.
@@ -44,6 +46,10 @@ export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts;
}
export interface RfqtFillData extends FillData {
quote: RfqtIndicativeQuoteResponse;
}
/**
* Represents an individual DEX sample from the sampler contract.
*/
@@ -130,6 +136,10 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
fills: CollapsedFill[];
}
export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts {
quoteRequestor?: QuoteRequestor;
}
/**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
*/
@@ -183,6 +193,7 @@ export interface GetMarketOrdersOpts {
* sources. Defaults to `true`.
*/
allowFallback: boolean;
rfqt?: GetMarketOrdersRfqtOpts;
/**
* Whether to combine contiguous bridge orders into a single DexForwarderBridge
* order. Defaults to `true`.

View File

@@ -12,6 +12,13 @@ import { MarketOperation, RfqtRequestOpts } from '../types';
* Request quotes from RFQ-T providers
*/
export interface RfqtIndicativeQuoteResponse {
makerAssetData: string;
makerAssetAmount: BigNumber;
takerAssetData: string;
takerAssetAmount: BigNumber;
}
function getTokenAddressOrThrow(assetData: string): string {
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (decodedAssetData.hasOwnProperty('tokenAddress')) {
@@ -141,4 +148,81 @@ export class QuoteRequestor {
return orders;
}
public async requestRfqtIndicativeQuotesAsync(
makerAssetData: string,
takerAssetData: string,
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
options: RfqtRequestOpts,
): Promise<RfqtIndicativeQuoteResponse[]> {
const _opts = _.merge({}, constants.DEFAULT_RFQT_REQUEST_OPTS, options);
assertTakerAddressOrThrow(_opts.takerAddress);
const axiosResponsesIfDefined: Array<
undefined | AxiosResponse<RfqtIndicativeQuoteResponse>
> = await Promise.all(
this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => {
try {
return await Axios.get<RfqtIndicativeQuoteResponse>(`${rfqtMakerEndpoint}/price`, {
headers: { '0x-api-key': options.apiKey },
params: {
takerAddress: options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
},
timeout: options.makerEndpointMaxResponseTimeMs,
});
} catch (err) {
logUtils.warn(
`Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${
options.apiKey
} for taker address ${options.takerAddress}`,
);
logUtils.warn(err);
return undefined;
}
}),
);
const axiosResponses = axiosResponsesIfDefined.filter(
(respIfDefd): respIfDefd is AxiosResponse<RfqtIndicativeQuoteResponse> => respIfDefd !== undefined,
);
const responsesWithStringInts = axiosResponses.map(response => response.data); // not yet BigNumber
const validResponsesWithStringInts = responsesWithStringInts.filter(response => {
if (this._isValidRfqtIndicativeQuoteResponse(response)) {
return true;
}
logUtils.warn(`Invalid RFQ-T indicative quote received, filtering out: ${JSON.stringify(response)}`);
return false;
});
const responses = validResponsesWithStringInts.map(response => {
return {
...response,
makerAssetAmount: new BigNumber(response.makerAssetAmount),
takerAssetAmount: new BigNumber(response.takerAssetAmount),
};
});
return responses;
}
private _isValidRfqtIndicativeQuoteResponse(response: RfqtIndicativeQuoteResponse): boolean {
const hasValidMakerAssetAmount = this._schemaValidator.isValid(
response.makerAssetAmount,
schemas.wholeNumberSchema,
);
const hasValidTakerAssetAmount = this._schemaValidator.isValid(
response.takerAssetAmount,
schemas.wholeNumberSchema,
);
const hasValidMakerAssetData = this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema);
const hasValidTakerAssetData = this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema);
if (hasValidMakerAssetAmount && hasValidTakerAssetAmount && hasValidMakerAssetData && hasValidTakerAssetData) {
return true;
}
return false;
}
}

View File

@@ -28,6 +28,27 @@ export const rfqtMocker = {
.replyOnce(responseCode, responseData);
}
await performFn();
} finally {
// Ensure we always restore axios afterwards
mockedAxios.restore();
}
},
withMockedRfqtIndicativeQuotes: async (
mockedResponses: MockedRfqtFirmQuoteResponse[],
performFn: () => Promise<void>,
) => {
const mockedAxios = new AxiosMockAdapter(axios);
try {
// Mock out RFQT responses
for (const mockedResponse of mockedResponses) {
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
mockedAxios
.onGet(`${endpoint}/price`, { params: requestParams }, requestHeaders)
.replyOnce(responseCode, responseData);
}
await performFn();
} finally {
// Ensure we always restore axios afterwards

View File

@@ -20,7 +20,7 @@ describe('QuoteRequestor', async () => {
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken);
describe('requestRfqtFirmQuotesAsync', async () => {
describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
it('should return successful RFQT requests', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key';