import { isAPIError , isRevertError } from '@0x/api-utils' ; import { ERC20BridgeSource , RfqRequestOpts , SwapQuoterError } from '@0x/asset-swapper' ; import { NATIVE_FEE_TOKEN_BY_CHAIN_ID , SELL_SOURCE_FILTER_BY_CHAIN_ID , } from '@0x/asset-swapper/lib/src/utils/market_operation_utils/constants' ; import { findTokenAddressOrThrow , getTokenMetadataIfExists , isNativeSymbolOrAddress , isNativeWrappedSymbolOrAddress , TokenMetadatasForChains , } from '@0x/token-metadata' ; import { MarketOperation } from '@0x/types' ; import { BigNumber , NULL_ADDRESS } from '@0x/utils' ; import * as express from 'express' ; import * as HttpStatus from 'http-status-codes' ; import { Kafka , Producer } from 'kafkajs' ; import _ = require ( 'lodash' ) ; import { Counter , Histogram } from 'prom-client' ; import { CHAIN_ID , getIntegratorByIdOrThrow , getIntegratorIdForApiKey , KAFKA_BROKERS , MATCHA_INTEGRATOR_ID , NATIVE_WRAPPED_TOKEN_SYMBOL , PLP_API_KEY_WHITELIST , RFQT_API_KEY_WHITELIST , RFQT_INTEGRATOR_IDS , RFQT_REGISTRY_PASSWORDS , } from '../config' ; import { AFFILIATE_DATA_SELECTOR , DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE , MARKET_DEPTH_DEFAULT_DISTRIBUTION , MARKET_DEPTH_MAX_SAMPLES , ONE_SECOND_MS , PROMETHEUS_REQUEST_BUCKETS , SWAP_DOCS_URL , } from '../constants' ; import { InternalServerError , RevertAPIError , ValidationError , ValidationErrorCodes , ValidationErrorReasons , } from '../errors' ; import { schemas } from '../schemas' ; import { SwapService } from '../services/swap_service' ; import { GetSwapPriceResponse , GetSwapQuoteParams , GetSwapQuoteResponse } from '../types' ; import { findTokenAddressOrThrowApiError } from '../utils/address_utils' ; import { paginationUtils } from '../utils/pagination_utils' ; import { parseUtils } from '../utils/parse_utils' ; import { priceComparisonUtils } from '../utils/price_comparison_utils' ; import { quoteReportUtils } from '../utils/quote_report_utils' ; import { schemaUtils } from '../utils/schema_utils' ; import { serviceUtils } from '../utils/service_utils' ; let kafkaProducer ; if ( KAFKA_BROKERS !== undefined ) { const kafka = new Kafka ( { clientId : '0x-api' , brokers : KAFKA_BROKERS , } ) ; kafkaProducer = kafka . producer ( ) ; kafkaProducer . connect ( ) ; } const BEARER_REGEX = /^Bearer\s(.{36})$/ ; const REGISTRY_SET = new Set ( RFQT_REGISTRY_PASSWORDS ) ; const REGISTRY_ENDPOINT_FETCHED = new Counter ( { name : 'swap_handler_registry_endpoint_fetched' , help : 'Requests to the swap handler' , labelNames : [ 'identifier' ] , } ) ; const HTTP_SWAP_RESPONSE_TIME = new Histogram ( { name : 'http_swap_response_time' , help : 'The response time of a HTTP Swap request' , buckets : PROMETHEUS_REQUEST_BUCKETS , } ) ; export class SwapHandlers { private readonly _swapService : SwapService ; public static root ( _req , res ) { const message = `This is the root of the Swap API. Visit ${ SWAP_DOCS_URL } for details about this API.` ; res . status ( HttpStatus . OK ) . send ( { message } ) ; } public static getTokens ( _req , res ) { const tokens = TokenMetadatasForChains . map ( ( tm ) => ( { symbol : tm . symbol , address : tm . tokenAddresses [ CHAIN_ID ] , name : tm . name , decimals : tm . decimals , } ) ) ; const filteredTokens = tokens . filter ( ( t ) => t . address !== NULL_ADDRESS ) ; res . status ( HttpStatus . OK ) . send ( { records : filteredTokens } ) ; } public static getLiquiditySources ( _req , res ) { const sources = SELL_SOURCE_FILTER_BY_CHAIN_ID [ CHAIN_ID ] . sources . map ( ( s ) => ( s === ERC20BridgeSource . Native ? '0x' : s ) ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ; res . status ( HttpStatus . OK ) . send ( { records : sources } ) ; } public static getRfqRegistry ( req , res ) { const auth = req . header ( 'Authorization' ) ; REGISTRY_ENDPOINT_FETCHED . labels ( auth || 'N/A' ) . inc ( ) ; if ( auth === undefined ) { return res . status ( HttpStatus . UNAUTHORIZED ) . end ( ) ; } const authTokenRegex = auth . match ( BEARER_REGEX ) ; if ( ! authTokenRegex ) { return res . status ( HttpStatus . UNAUTHORIZED ) . end ( ) ; } const authToken = authTokenRegex [ 1 ] ; if ( ! REGISTRY_SET . has ( authToken ) ) { return res . status ( HttpStatus . UNAUTHORIZED ) . end ( ) ; } res . status ( HttpStatus . OK ) . send ( RFQT_INTEGRATOR_IDS ) . end ( ) ; } constructor ( swapService ) { this . _swapService = swapService ; } public async getTokenPricesAsync ( req , res ) { const symbolOrAddress = ( req . query . sellToken as string ) || 'WETH' ; const baseAsset = getTokenMetadataIfExists ( symbolOrAddress , CHAIN_ID ) ; if ( ! baseAsset ) { throw new ValidationError ( [ { field : 'sellToken' , code : ValidationErrorCodes . ValueOutOfRange , reason : `Could not find token ${ symbolOrAddress }` , } , ] ) ; } const { page , perPage } = paginationUtils . parsePaginationConfig ( req ) ; const unitAmount = new BigNumber ( 1 ) ; const tokenPrices = await this . _swapService . getTokenPricesAsync ( baseAsset , unitAmount , page , perPage ) ; res . status ( HttpStatus . OK ) . send ( tokenPrices ) ; } public async getQuoteAsync ( req , res ) { const begin = Date . now ( ) ; const params = parseSwapQuoteRequestParams ( req , 'quote' ) ; const quote = await this . _getSwapQuoteAsync ( params , req ) ; if ( params . rfqt !== undefined ) { req . log . info ( { firmQuoteServed : { taker : params . takerAddress , apiKey : params . integrator ?. integratorId , integratorId : params . integrator ?. integratorId , rawApiKey : params . apiKey , buyToken : params . buyToken , sellToken : params . sellToken , buyAmount : params . buyAmount , sellAmount : params . sellAmount , } , } ) ; if ( quote . quoteReport && params . rfqt && params . rfqt . intentOnFilling ) { quoteReportUtils . logQuoteReport ( { quoteReport : quote . quoteReport , submissionBy : 'taker' , decodedUniqueId : quote . decodedUniqueId , buyTokenAddress : quote . buyTokenAddress , sellTokenAddress : quote . sellTokenAddress , buyAmount : params . buyAmount , sellAmount : params . sellAmount , integratorId : params . integrator ?. integratorId , slippage : undefined , } , req . log ) ; } if ( quote . extendedQuoteReportSources && kafkaProducer ) { const quoteId = getQuoteIdFromSwapQuote ( quote ) ; quoteReportUtils . publishQuoteReport ( { quoteId , taker : params . takerAddress , quoteReportSources : quote . extendedQuoteReportSources , submissionBy : 'taker' , decodedUniqueId : quote . decodedUniqueId , buyTokenAddress : quote . buyTokenAddress , sellTokenAddress : quote . sellTokenAddress , buyAmount : params . buyAmount , sellAmount : params . sellAmount , integratorId : params . integrator ?. integratorId , slippage : params . slippagePercentage , } , true , kafkaProducer ) ; } } const response = _ . omit ( { ... quote , orders : quote . orders . map ( ( o ) => _ . omit ( o , 'fills' ) ) , } , 'quoteReport' , 'extendedQuoteReportSources' , 'priceComparisonsReport' , 'decodedUniqueId' ) ; if ( params . includePriceComparisons && quote . priceComparisonsReport ) { const side = params . sellAmount ? MarketOperation . Sell : MarketOperation . Buy ; const priceComparisons = priceComparisonUtils . getPriceComparisonFromQuote ( CHAIN_ID , side , quote ) ; response . priceComparisons = priceComparisons ?. map ( ( sc ) => priceComparisonUtils . renameNative ( sc ) ) ; } const duration = ( new Date ( ) . getTime ( ) - begin ) / ONE_SECOND_MS ; HTTP_SWAP_RESPONSE_TIME . observe ( duration ) ; res . status ( HttpStatus . OK ) . send ( response ) ; } public async getQuotePriceAsync ( req , res ) { const params = parseSwapQuoteRequestParams ( req , 'price' ) ; const quote = await this . _getSwapQuoteAsync ( { ... params } , req ) ; req . log . info ( { indicativeQuoteServed : { taker : params . takerAddress , apiKey : params . integrator ?. integratorId , integratorId : params . integrator ?. integratorId , rawApiKey : params . apiKey , buyToken : params . buyToken , sellToken : params . sellToken , buyAmount : params . buyAmount , sellAmount : params . sellAmount , } , } ) ; const response = _ . pick ( quote , 'chainId' , 'price' , 'value' , 'gasPrice' , 'gas' , 'estimatedGas' , 'protocolFee' , 'minimumProtocolFee' , 'buyTokenAddress' , 'buyAmount' , 'sellTokenAddress' , 'sellAmount' , 'sources' , 'allowanceTarget' , 'sellTokenToEthRate' , 'buyTokenToEthRate' ) ; if ( params . includePriceComparisons && quote . priceComparisonsReport ) { const marketSide = params . sellAmount ? MarketOperation . Sell : MarketOperation . Buy ; response . priceComparisons = priceComparisonUtils . getPriceComparisonFromQuote ( CHAIN_ID , marketSide , quote ) ?. map ( ( sc ) => priceComparisonUtils . renameNative ( sc ) ) ; } if ( quote . extendedQuoteReportSources && kafkaProducer ) { const quoteId = getQuoteIdFromSwapQuote ( quote ) ; quoteReportUtils . publishQuoteReport ( { quoteId , taker : params . takerAddress , quoteReportSources : quote . extendedQuoteReportSources , submissionBy : 'taker' , decodedUniqueId : quote . decodedUniqueId , buyTokenAddress : quote . buyTokenAddress , sellTokenAddress : quote . sellTokenAddress , buyAmount : params . buyAmount , sellAmount : params . sellAmount , integratorId : params . integrator ?. integratorId , slippage : params . slippagePercentage , } , false , kafkaProducer ) ; } res . status ( HttpStatus . OK ) . send ( response ) ; } public async getMarketDepthAsync ( req , res ) { const buyTokenSymbolOrAddress = isNativeSymbolOrAddress ( req . query . buyToken as string , CHAIN_ID ) ? NATIVE_WRAPPED_TOKEN_SYMBOL : ( req . query . buyToken as string ) ; const sellTokenSymbolOrAddress = isNativeSymbolOrAddress ( req . query . sellToken as string , CHAIN_ID ) ? NATIVE_WRAPPED_TOKEN_SYMBOL : ( req . query . sellToken as string ) ; if ( buyTokenSymbolOrAddress === sellTokenSymbolOrAddress ) { throw new ValidationError ( [ { field : 'buyToken' , code : ValidationErrorCodes . InvalidAddress , reason : `Invalid pair ${ sellTokenSymbolOrAddress }/${ buyTokenSymbolOrAddress }` , } , ] ) ; } const response = await this . _swapService . calculateMarketDepthAsync ( { buyToken : findTokenAddressOrThrow ( buyTokenSymbolOrAddress , CHAIN_ID ) , sellToken : findTokenAddressOrThrow ( sellTokenSymbolOrAddress , CHAIN_ID ) , sellAmount : new BigNumber ( req . query . sellAmount as string ) , numSamples : req . query . numSamples ? parseInt ( req . query . numSamples as string ) : MARKET_DEPTH_MAX_SAMPLES , sampleDistributionBase : req . query . sampleDistributionBase ? parseFloat ( req . query . sampleDistributionBase as string ) : MARKET_DEPTH_DEFAULT_DISTRIBUTION , excludedSources : req . query . excludedSources === undefined ? [ ] : parseUtils . parseStringArrForERC20BridgeSources ( ( req . query . excludedSources as string ) . split ( ',' ) ) , includedSources : req . query . includedSources === undefined ? [ ] : parseUtils . parseStringArrForERC20BridgeSources ( ( req . query . includedSources as string ) . split ( ',' ) ) , } ) ; res . status ( HttpStatus . OK ) . send ( response ) ; } private async _getSwapQuoteAsync ( params , req ) { try { let swapQuote ; if ( params . isUnwrap ) { swapQuote = await this . _swapService . getSwapQuoteForUnwrapAsync ( params ) ; } else if ( params . isWrap ) { swapQuote = await this . _swapService . getSwapQuoteForWrapAsync ( params ) ; } else { swapQuote = await this . _swapService . calculateSwapQuoteAsync ( params ) ; } return swapQuote ; } catch ( e ) { if ( isAPIError ( e ) ) { throw e ; } if ( isRevertError ( e ) ) { throw new RevertAPIError ( e ) ; } const errorMessage = e . message ; if ( errorMessage . startsWith ( SwapQuoterError . InsufficientAssetLiquidity ) || errorMessage . startsWith ( 'NO_OPTIMAL_PATH' ) ) { throw new ValidationError ( [ { field : params . sellAmount ? 'sellAmount' : 'buyAmount' , code : ValidationErrorCodes . ValueOutOfRange , reason : SwapQuoterError . InsufficientAssetLiquidity , } , ] ) ; } if ( errorMessage . startsWith ( SwapQuoterError . AssetUnavailable ) ) { throw new ValidationError ( [ { field : 'token' , code : ValidationErrorCodes . ValueOutOfRange , reason : e . message , } , ] ) ; } throw new InternalServerError ( e . message ) ; } } } const parseSwapQuoteRequestParams = ( req , endpoint ) : GetSwapQuoteParams => { schemaUtils . validateSchema ( req . query , schemas . swapQuoteRequestSchema as any ) ; const apiKey = req . header ( '0x-api-key' ) ; let integratorId ; if ( apiKey ) { integratorId = getIntegratorIdForApiKey ( apiKey ) ; } const { takerAddress , affiliateAddress } = req . query ; let skipValidation ; skipValidation = req . query . skipValidation === undefined ? false : req . query . skipValidation === 'true' ; if ( endpoint === 'quote' && integratorId !== undefined && integratorId === MATCHA_INTEGRATOR_ID ) { skipValidation = false ; } const includePriceComparisons = req . query . includePriceComparisons === 'true' ? true : false ; const shouldSellEntireBalance = req . query . shouldSellEntireBalance === 'true' ? true : false ; const sellTokenRaw = req . query . sellToken as string ; const buyTokenRaw = req . query . buyToken as string ; const isNativeSell = isNativeSymbolOrAddress ( sellTokenRaw , CHAIN_ID ) ; const isNativeBuy = isNativeSymbolOrAddress ( buyTokenRaw , CHAIN_ID ) ; const sellToken = findTokenAddressOrThrowApiError ( isNativeSell ? NATIVE_FEE_TOKEN_BY_CHAIN_ID [ CHAIN_ID ] : sellTokenRaw , 'sellToken' , CHAIN_ID ) . toLowerCase ( ) ; const buyToken = findTokenAddressOrThrowApiError ( isNativeBuy ? NATIVE_FEE_TOKEN_BY_CHAIN_ID [ CHAIN_ID ] : buyTokenRaw , 'buyToken' , CHAIN_ID ) . toLowerCase ( ) ; const isWrap = isNativeSell && isNativeWrappedSymbolOrAddress ( buyToken , CHAIN_ID ) ; const isUnwrap = isNativeWrappedSymbolOrAddress ( sellToken , CHAIN_ID ) && isNativeBuy ; if ( ! isUnwrap && ! isWrap && sellToken === buyToken ) { throw new ValidationError ( [ 'buyToken' , 'sellToken' ] . map ( ( field ) => { return { field , code : ValidationErrorCodes . RequiredField , reason : 'buyToken and sellToken must be different' , } ; } ) ) ; } if ( sellToken === NULL_ADDRESS || buyToken === NULL_ADDRESS ) { throw new ValidationError ( [ 'buyToken' , 'sellToken' ] . map ( ( field ) => { return { field , code : ValidationErrorCodes . FieldInvalid , reason : 'Invalid token combination' , } ; } ) ) ; } const sellAmount = req . query . sellAmount === undefined ? undefined : new BigNumber ( req . query . sellAmount as string ) ; const buyAmount = req . query . buyAmount === undefined ? undefined : new BigNumber ( req . query . buyAmount as string ) ; const gasPrice = req . query . gasPrice === undefined ? undefined : new BigNumber ( req . query . gasPrice as string ) ; const slippagePercentage = req . query . slippagePercentage === undefined ? DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE : Number . parseFloat ( req . query . slippagePercentage as string ) ; if ( slippagePercentage > 1 ) { throw new ValidationError ( [ { field : 'slippagePercentage' , code : ValidationErrorCodes . ValueOutOfRange , reason : ValidationErrorReasons . PercentageOutOfRange , } , ] ) ; } const { excludedSources , includedSources , nativeExclusivelyRFQT } = parseUtils . parseRequestForExcludedSources ( { excludedSources : req . query . excludedSources as string | undefined , includedSources : req . query . includedSources as string | undefined , intentOnFilling : req . query . intentOnFilling as string | undefined , takerAddress : takerAddress as string , apiKey , } , RFQT_API_KEY_WHITELIST , endpoint ) ; const updatedExcludedSources = serviceUtils . determineExcludedSources ( excludedSources , apiKey , PLP_API_KEY_WHITELIST ) ; const isAllExcluded = Object . values ( ERC20BridgeSource ) . every ( ( s ) => updatedExcludedSources . includes ( s ) ) ; if ( isAllExcluded ) { throw new ValidationError ( [ { field : 'excludedSources' , code : ValidationErrorCodes . ValueOutOfRange , reason : 'Request excluded all sources' , } , ] ) ; } req . log . info ( { type : 'swapRequest' , endpoint , updatedExcludedSources , nativeExclusivelyRFQT , apiKey : integratorId || 'N/A' , integratorId : integratorId || 'N/A' , rawApiKey : apiKey || 'N/A' , } ) ; const rfqt = ( ( ) => { if ( apiKey ) { if ( endpoint === 'quote' && takerAddress ) { return { intentOnFilling : req . query . intentOnFilling === 'true' , isIndicative : false , nativeExclusivelyRFQT , } ; } else if ( endpoint === 'price' ) { return { intentOnFilling : false , isIndicative : true , nativeExclusivelyRFQT , } ; } } return undefined ; } ) ( ) ; const affiliateFee = parseUtils . parseAffiliateFeeOptions ( req ) ; const integrator = integratorId ? getIntegratorByIdOrThrow ( integratorId ) : undefined ; return { takerAddress : takerAddress as string , sellToken , buyToken , sellAmount , buyAmount , slippagePercentage , gasPrice , excludedSources : updatedExcludedSources , includedSources , affiliateAddress : affiliateAddress as string , rfqt , skipValidation , apiKey , integrator , affiliateFee , includePriceComparisons , shouldSellEntireBalance , isMetaTransaction : false , isETHSell : isNativeSell , isETHBuy : isNativeBuy , isUnwrap , isWrap , } ; } ; function getQuoteIdFromSwapQuote ( quote ) { const bytesPos = quote . data . indexOf ( AFFILIATE_DATA_SELECTOR ) ; const quoteIdOffset = 118 ; const startingIndex = bytesPos + quoteIdOffset ; const quoteId = quote . data . slice ( startingIndex , startingIndex + 10 ) ; return quoteId ; }