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:
@@ -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';
|
||||
|
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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?
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -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`.
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user