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, CollapsedFill,
NativeCollapsedFill, NativeCollapsedFill,
OptimizedMarketOrder, OptimizedMarketOrder,
GetMarketOrdersRfqtOpts,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_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'; export { rfqtMocker } from './utils/rfqt_mocker';

View File

@@ -9,6 +9,7 @@ import * as _ from 'lodash';
import { constants } from './constants'; import { constants } from './constants';
import { import {
CalculateSwapQuoteOpts,
LiquidityForTakerMakerAssetDataPair, LiquidityForTakerMakerAssetDataPair,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation, MarketOperation,
@@ -24,7 +25,6 @@ import { calculateLiquidity } from './utils/calculate_liquidity';
import { MarketOperationUtils } from './utils/market_operation_utils'; import { MarketOperationUtils } from './utils/market_operation_utils';
import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders';
import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { GetMarketOrdersOpts } from './utils/market_operation_utils/types';
import { orderPrunerUtils } from './utils/order_prune_utils'; import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils'; import { OrderStateUtils } from './utils/order_state_utils';
import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils';
@@ -565,19 +565,30 @@ export class SwapQuoter {
let swapQuote: SwapQuote; 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) { if (marketOperation === MarketOperation.Buy) {
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
orders, orders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts, calcOpts,
); );
} else { } else {
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
orders, orders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts, calcOpts,
); );
} }

View File

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

View File

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

View File

@@ -12,6 +12,13 @@ import { MarketOperation, RfqtRequestOpts } from '../types';
* Request quotes from RFQ-T providers * Request quotes from RFQ-T providers
*/ */
export interface RfqtIndicativeQuoteResponse {
makerAssetData: string;
makerAssetAmount: BigNumber;
takerAssetData: string;
takerAssetAmount: BigNumber;
}
function getTokenAddressOrThrow(assetData: string): string { function getTokenAddressOrThrow(assetData: string): string {
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (decodedAssetData.hasOwnProperty('tokenAddress')) { if (decodedAssetData.hasOwnProperty('tokenAddress')) {
@@ -141,4 +148,81 @@ export class QuoteRequestor {
return orders; 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); .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(); await performFn();
} finally { } finally {
// Ensure we always restore axios afterwards // Ensure we always restore axios afterwards

View File

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