diff --git a/src/constants.ts b/src/constants.ts index af9101cc2..f5a747d9d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -95,3 +95,6 @@ export const GST2_WALLET_ADDRESSES = { export const MARKET_DEPTH_MAX_SAMPLES = 50; export const MARKET_DEPTH_DEFAULT_DISTRIBUTION = 1.05; export const MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC = 20; + +// Logging +export const NUMBER_SOURCES_PER_LOG_LINE = 12; diff --git a/src/handlers/swap_handlers.ts b/src/handlers/swap_handlers.ts index 9024e4e64..d20595455 100644 --- a/src/handlers/swap_handlers.ts +++ b/src/handlers/swap_handlers.ts @@ -2,6 +2,7 @@ import { RfqtRequestOpts, SwapQuoterError } from '@0x/asset-swapper'; import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import * as express from 'express'; import * as HttpStatus from 'http-status-codes'; +import _ = require('lodash'); import { CHAIN_ID, RFQT_API_KEY_WHITELIST } from '../config'; import { @@ -34,6 +35,8 @@ import { isWETHSymbolOrAddress, } from '../utils/token_metadata_utils'; +import { quoteReportUtils } from './../utils/quote_report_utils'; + export class SwapHandlers { private readonly _swapService: SwapService; public static rootAsync(_req: express.Request, res: express.Response): void { @@ -63,8 +66,16 @@ export class SwapHandlers { makers: quote.orders.map(order => order.makerAddress), }, }); + if (quote.quoteReport && params.rfqt && params.rfqt.intentOnFilling) { + quoteReportUtils.logQuoteReport({ + quoteReport: quote.quoteReport, + submissionBy: 'taker', + decodedUniqueId: quote.decodedUniqueId, + }); + } } - res.status(HttpStatus.OK).send(quote); + const cleanedQuote = _.omit(quote, 'quoteReport', 'decodedUniqueId'); + res.status(HttpStatus.OK).send(cleanedQuote); } // tslint:disable-next-line:prefer-function-over-method public async getSwapTokensAsync(_req: express.Request, res: express.Response): Promise { diff --git a/src/services/meta_transaction_service.ts b/src/services/meta_transaction_service.ts index 58005c914..15ad9e2f1 100644 --- a/src/services/meta_transaction_service.ts +++ b/src/services/meta_transaction_service.ts @@ -1,4 +1,11 @@ -import { Orderbook, SwapQuoter, SwapQuoteRequestOpts, SwapQuoterOpts } from '@0x/asset-swapper'; +import { + MarketBuySwapQuote, + MarketSellSwapQuote, + Orderbook, + SwapQuoter, + SwapQuoteRequestOpts, + SwapQuoterOpts, +} from '@0x/asset-swapper'; import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { ContractWrappers } from '@0x/contract-wrappers'; import { DevUtilsContract } from '@0x/contracts-dev-utils'; @@ -41,6 +48,7 @@ import { ZeroExTransactionWithoutDomain, } from '../types'; import { ethGasStationUtils } from '../utils/gas_station_utils'; +import { quoteReportUtils } from '../utils/quote_report_utils'; import { serviceUtils } from '../utils/service_utils'; import { utils } from '../utils/utils'; @@ -111,7 +119,7 @@ export class MetaTransactionService { rfqt: _rfqt, }; - let swapQuote; + let swapQuote: MarketSellSwapQuote | MarketBuySwapQuote | undefined; if (sellAmount !== undefined) { swapQuote = await this._swapQuoter.getMarketSellSwapQuoteAsync( buyTokenAddress, @@ -129,7 +137,7 @@ export class MetaTransactionService { } else { throw new Error('sellAmount or buyAmount required'); } - const { gasPrice } = swapQuote; + const { gasPrice, quoteReport } = swapQuote; const { gas, protocolFeeInWeiAmount: protocolFee } = swapQuote.worstCaseQuoteInfo; const makerAssetAmount = swapQuote.bestCaseQuoteInfo.makerAssetAmount; const totalTakerAssetAmount = swapQuote.bestCaseQuoteInfo.totalTakerAssetAmount; @@ -162,6 +170,7 @@ export class MetaTransactionService { protocolFee, minimumProtocolFee: protocolFee, allowanceTarget, + quoteReport, }; return response; } @@ -177,6 +186,7 @@ export class MetaTransactionService { estimatedGas, protocolFee, minimumProtocolFee, + quoteReport, } = await this.calculateMetaTransactionPriceAsync(params, 'quote'); const floatGasPrice = swapQuote.gasPrice; @@ -206,6 +216,11 @@ export class MetaTransactionService { ) .callAsync(); + // log quote report and associate with txn hash if this is an RFQT firm quote + if (quoteReport) { + quoteReportUtils.logQuoteReport({ submissionBy: 'metaTxn', quoteReport, zeroExTransactionHash }); + } + const makerAssetAmount = swapQuote.bestCaseQuoteInfo.makerAssetAmount; const totalTakerAssetAmount = swapQuote.bestCaseQuoteInfo.totalTakerAssetAmount; const allowanceTarget = this._contractAddresses.erc20Proxy; @@ -339,7 +354,7 @@ export class MetaTransactionService { status: TransactionStates.Unsubmitted, takerAddress: zeroExTransaction.signerAddress, to: this._contractWrappers.exchange.address, - data, + data: data.affiliatedData, value: protocolFee, apiKey, gasPrice: zeroExTransaction.gasPrice, diff --git a/src/services/swap_service.ts b/src/services/swap_service.ts index a90421e89..bfb2fbf65 100644 --- a/src/services/swap_service.ts +++ b/src/services/swap_service.ts @@ -121,14 +121,14 @@ export class SwapService { protocolFeeInWeiAmount: bestCaseProtocolFee, } = attributedSwapQuote.bestCaseQuoteInfo; const { protocolFeeInWeiAmount: protocolFee, gas: worstCaseGas } = attributedSwapQuote.worstCaseQuoteInfo; - const { orders, gasPrice, sourceBreakdown } = attributedSwapQuote; + const { orders, gasPrice, sourceBreakdown, quoteReport } = attributedSwapQuote; const { gasCost: affiliateFeeGasCost, buyTokenFeeAmount, sellTokenFeeAmount, } = serviceUtils.getAffiliateFeeAmounts(swapQuote, affiliateFee); - const { to, value, data } = await this._getSwapQuotePartialTransactionAsync( + const { to, value, data, decodedUniqueId } = await this._getSwapQuotePartialTransactionAsync( swapQuote, isETHSell, isETHBuy, @@ -221,6 +221,8 @@ export class SwapService { sources: serviceUtils.convertSourceBreakdownToArray(sourceBreakdown), orders: serviceUtils.cleanSignedOrderFields(orders), allowanceTarget, + decodedUniqueId, + quoteReport, }; return apiSwapQuote; } @@ -360,7 +362,7 @@ export class SwapService { : this._wethContract.deposit() ).getABIEncodedTransactionData(); const value = isUnwrap ? ZERO : amount; - const affiliatedData = serviceUtils.attributeCallData(data, affiliateAddress); + const attributedCalldata = serviceUtils.attributeCallData(data, affiliateAddress); // TODO: consider not using protocol fee utils due to lack of need for an aggresive gas price for wrapping/unwrapping const gasPrice = providedGasPrice || (await this._swapQuoter.getGasPriceEstimationOrThrowAsync()); const gasEstimate = isUnwrap ? UNWRAP_QUOTE_GAS : WRAP_QUOTE_GAS; @@ -368,7 +370,8 @@ export class SwapService { price: ONE, guaranteedPrice: ONE, to: this._wethContract.address, - data: affiliatedData, + data: attributedCalldata.affiliatedData, + decodedUniqueId: attributedCalldata.decodedUniqueId, value, gas: gasEstimate, estimatedGas: gasEstimate, @@ -537,11 +540,12 @@ export class SwapService { toAddress: to, } = await this._swapQuoteConsumer.getCalldataOrThrowAsync(swapQuote, opts); - const affiliatedData = serviceUtils.attributeCallData(data, affiliateAddress); + const { affiliatedData, decodedUniqueId } = serviceUtils.attributeCallData(data, affiliateAddress); return { to, value, data: affiliatedData, + decodedUniqueId, }; } diff --git a/src/types.ts b/src/types.ts index 1c92644ca..3baf6826c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { ERC20BridgeSource, MarketBuySwapQuote, MarketSellSwapQuote, + QuoteReport, RfqtRequestOpts, SupportedProvider, } from '@0x/asset-swapper'; @@ -395,6 +396,7 @@ export interface SwapQuoteResponsePartialTransaction { to: string; data: string; value: BigNumber; + decodedUniqueId: string; } export interface SwapQuoteResponsePrice { @@ -417,6 +419,7 @@ export interface GetSwapQuoteResponse extends SwapQuoteResponsePartialTransactio estimatedGas: BigNumber; estimatedGasTokenRefund: BigNumber; allowanceTarget?: string; + quoteReport?: QuoteReport; } export interface Price { @@ -476,6 +479,7 @@ export interface CalculateMetaTransactionPriceResponse { protocolFee: BigNumber; minimumProtocolFee: BigNumber; estimatedGas: BigNumber; + quoteReport?: QuoteReport; allowanceTarget?: string; } diff --git a/src/utils/quote_report_utils.ts b/src/utils/quote_report_utils.ts new file mode 100644 index 000000000..f82266954 --- /dev/null +++ b/src/utils/quote_report_utils.ts @@ -0,0 +1,53 @@ +import { QuoteReport } from '@0x/asset-swapper'; +import _ = require('lodash'); + +import { NUMBER_SOURCES_PER_LOG_LINE } from '../constants'; +import { logger } from '../logger'; + +interface QuoteReportForTakerTxn { + quoteReport: QuoteReport; + submissionBy: 'taker'; + decodedUniqueId: string; +} +interface QuoteReportForMetaTxn { + quoteReport: QuoteReport; + submissionBy: 'metaTxn'; + zeroExTransactionHash: string; +} +type QuoteReportLogOptions = QuoteReportForTakerTxn | QuoteReportForMetaTxn; + +export const quoteReportUtils = { + logQuoteReport(logOpts: QuoteReportLogOptions): void { + const qr = logOpts.quoteReport; + + let logBase: { [key: string]: string | boolean } = { + firmQuoteReport: true, + submissionBy: logOpts.submissionBy, + }; + if (logOpts.submissionBy === 'metaTxn') { + logBase = { ...logBase, zeroExTransactionHash: logOpts.zeroExTransactionHash }; + } else if (logOpts.submissionBy === 'taker') { + logBase = { ...logBase, decodedUniqueId: logOpts.decodedUniqueId }; + } + + // Deliver in chunks since Kibana can't handle logs large requests + const sourcesConsideredChunks = _.chunk(qr.sourcesConsidered, NUMBER_SOURCES_PER_LOG_LINE); + sourcesConsideredChunks.forEach((chunk, i) => { + logger.info({ + ...logBase, + sourcesConsidered: chunk, + sourcesConsideredChunkIndex: i, + sourcesConsideredChunkLength: sourcesConsideredChunks.length, + }); + }); + const sourcesDeliveredChunks = _.chunk(qr.sourcesDelivered, NUMBER_SOURCES_PER_LOG_LINE); + sourcesDeliveredChunks.forEach((chunk, i) => { + logger.info({ + ...logBase, + sourcesDelivered: chunk, + sourcesDeliveredChunkIndex: i, + sourcesDeliveredChunkLength: sourcesDeliveredChunks.length, + }); + }); + }, +}; diff --git a/src/utils/service_utils.ts b/src/utils/service_utils.ts index cd41cfc32..51bc0bb95 100644 --- a/src/utils/service_utils.ts +++ b/src/utils/service_utils.ts @@ -60,7 +60,13 @@ export const serviceUtils = { return attributedSwapQuote; }, - attributeCallData(data: string, affiliateAddress?: string): string { + attributeCallData( + data: string, + affiliateAddress?: string, + ): { + affiliatedData: string; + decodedUniqueId: string; + } { const affiliateAddressOrDefault = affiliateAddress ? affiliateAddress : FEE_RECIPIENT_ADDRESS; const affiliateCallDataEncoder = new AbiEncoder.Method({ constant: true, @@ -89,7 +95,7 @@ export const serviceUtils = { // Encode additional call data and return const encodedAffiliateData = affiliateCallDataEncoder.encode([affiliateAddressOrDefault, uniqueIdentifier]); const affiliatedData = `${data}${encodedAffiliateData.slice(2)}`; - return affiliatedData; + return { affiliatedData, decodedUniqueId: `${randomNumber}-${timestampInSeconds}` }; }, // tslint:disable-next-line:prefer-function-over-method diff --git a/test/service_utils_test.ts b/test/service_utils_test.ts index 595bca9ff..6a8ff6b9c 100644 --- a/test/service_utils_test.ts +++ b/test/service_utils_test.ts @@ -103,7 +103,7 @@ describe(SUITE_NAME, () => { it('it returns a reasonable ID and timestamp', () => { const fakeCallData = '0x0000000000000'; const fakeAffiliate = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; - const attributedCallData = serviceUtils.attributeCallData(fakeCallData, fakeAffiliate); + const attributedCallData = serviceUtils.attributeCallData(fakeCallData, fakeAffiliate).affiliatedData; const currentTime = new Date(); // parse out items from call data to ensure they are reasonable values