import { isAPIError , isRevertError } from '@0x/api-utils' ; import { assert } from '@0x/assert' ; import { ERC20BridgeSource , Signature , SwapQuoterError } from '@0x/asset-swapper' ; import { ChainId } from '@0x/contract-addresses' ; import { getTokenMetadataIfExists , isNativeSymbolOrAddress } from '@0x/token-metadata' ; import { MarketOperation } from '@0x/types' ; import { BigNumber } from '@0x/utils' ; import * as express from 'express' ; import * as HttpStatus from 'http-status-codes' ; import * as _ from 'lodash' ; import * as isValidUUID from 'uuid-validate' ; import { CHAIN_ID } from '../config' ; import { API_KEY_HEADER , DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE , META_TRANSACTION_DOCS_URL } from '../constants' ; import { TransactionEntity } from '../entities' ; import { APIErrorCodes , apiErrorCodesToReasons , EthSellNotSupportedError , InternalServerError , InvalidAPIKeyError , NotFoundError , RevertAPIError , ValidationError , ValidationErrorCodes , ValidationErrorReasons , } from '../errors' ; import { logger } from '../logger' ; import { schemas } from '../schemas' ; import { MetaTransactionService } from '../services/meta_transaction_service' ; import { ExchangeProxyMetaTransactionWithoutDomain , GetMetaTransactionPriceResponse , GetMetaTransactionStatusResponse , GetTransactionRequestParams , } from '../types' ; import { findTokenAddressOrThrowApiError } from '../utils/address_utils' ; import { parseUtils } from '../utils/parse_utils' ; import { priceComparisonUtils } from '../utils/price_comparison_utils' ; import { isRateLimitedMetaTransactionResponse , MetaTransactionRateLimiter } from '../utils/rate-limiters' ; import { schemaUtils } from '../utils/schema_utils' ; export class MetaTransactionHandlers { private readonly _metaTransactionService : MetaTransactionService ; private readonly _rateLimiter ? : MetaTransactionRateLimiter ; public static rootAsync ( _req , res ) { const message = `This is the root of the Meta Transaction API. Visit ${ META_TRANSACTION_DOCS_URL } for details about this API.` ; res . status ( HttpStatus . OK ) . send ( { message } ) ; } constructor ( metaTransactionService , rateLimiter ? ) { this . _metaTransactionService = metaTransactionService ; this . _rateLimiter = rateLimiter ; } public async getQuoteAsync ( req , res ) { const apiKey = req . header ( API_KEY_HEADER ) ; if ( apiKey !== undefined && ! isValidUUID ( apiKey ) ) { throw new InvalidAPIKeyError ( ) ; } schemaUtils . validateSchema ( req . query , schemas . metaTransactionQuoteRequestSchema as any ) ; const params = parseGetTransactionRequestParams ( req ) ; const { buyTokenAddress , sellTokenAddress } = params ; const isETHBuy = isNativeSymbolOrAddress ( buyTokenAddress , CHAIN_ID ) ; if ( isNativeSymbolOrAddress ( sellTokenAddress , CHAIN_ID ) ) { throw new EthSellNotSupportedError ( ) ; } try { const metaTransactionQuote = await this . _metaTransactionService . calculateMetaTransactionQuoteAsync ( { ... params , apiKey , isETHBuy , isETHSell : false , from : params . takerAddress , } ) ; const { quoteReport , ... quoteResponse } = metaTransactionQuote ; if ( params . includePriceComparisons && quoteReport ) { const marketSide = params . sellAmount !== undefined ? MarketOperation . Sell : MarketOperation . Buy ; quoteResponse . priceComparisons = priceComparisonUtils . getPriceComparisonFromQuote ( CHAIN_ID , marketSide , metaTransactionQuote ) ?. map ( ( sc ) => priceComparisonUtils . renameNative ( sc ) ) ; } res . status ( HttpStatus . OK ) . send ( quoteResponse ) ; } 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 . buyAmount ? 'buyAmount' : 'sellAmount' , code : ValidationErrorCodes . ValueOutOfRange , reason : SwapQuoterError . InsufficientAssetLiquidity , } , ] ) ; } if ( errorMessage . startsWith ( SwapQuoterError . AssetUnavailable ) ) { throw new ValidationError ( [ { field : 'token' , code : ValidationErrorCodes . ValueOutOfRange , reason : e . message , } , ] ) ; } logger . info ( 'Uncaught error' , e ) ; throw new InternalServerError ( e . message ) ; } } public async getPriceAsync ( req , res ) { const apiKey = req . header ( '0x-api-key' ) ; if ( apiKey !== undefined && ! isValidUUID ( apiKey ) ) { throw new InvalidAPIKeyError ( ) ; return ; } schemaUtils . validateSchema ( req . query , schemas . metaTransactionQuoteRequestSchema as any ) ; const params = parseGetTransactionRequestParams ( req ) ; const { buyTokenAddress , sellTokenAddress } = params ; if ( isNativeSymbolOrAddress ( sellTokenAddress , CHAIN_ID ) ) { throw new EthSellNotSupportedError ( ) ; } const isETHBuy = isNativeSymbolOrAddress ( buyTokenAddress , CHAIN_ID ) ; try { const metaTransactionPriceCalculation = await this . _metaTransactionService . calculateMetaTransactionPriceAsync ( { ... params , from : params . takerAddress , apiKey , isETHBuy , isETHSell : false , } ) ; const metaTransactionPriceResponse = { ... _ . omit ( metaTransactionPriceCalculation , 'orders' , 'quoteReport' , 'estimatedGasTokenRefund' ) , value : metaTransactionPriceCalculation . protocolFee , gas : metaTransactionPriceCalculation . estimatedGas , } ; const { quoteReport } = metaTransactionPriceCalculation ; if ( params . includePriceComparisons && quoteReport ) { const marketSide = params . sellAmount !== undefined ? MarketOperation . Sell : MarketOperation . Buy ; const priceComparisons = priceComparisonUtils . getPriceComparisonFromQuote ( CHAIN_ID , marketSide , metaTransactionPriceCalculation ) ; metaTransactionPriceResponse . priceComparisons = priceComparisons ?. map ( ( pc ) => priceComparisonUtils . renameNative ( pc ) ) ; } res . status ( HttpStatus . OK ) . send ( metaTransactionPriceResponse ) ; } 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 . buyAmount ? 'buyAmount' : 'sellAmount' , code : ValidationErrorCodes . ValueOutOfRange , reason : SwapQuoterError . InsufficientAssetLiquidity , } , ] ) ; } if ( errorMessage . startsWith ( SwapQuoterError . AssetUnavailable ) ) { throw new ValidationError ( [ { field : 'token' , code : ValidationErrorCodes . ValueOutOfRange , reason : e . message , } , ] ) ; } logger . info ( 'Uncaught error' , e ) ; throw new InternalServerError ( e . message ) ; } } public async submitTransactionIfWhitelistedAsync ( req , res ) { const apiKey = req . header ( '0x-api-key' ) ; const affiliateAddress = req . query . affiliateAddress as string | undefined ; if ( apiKey !== undefined && ! isValidUUID ( apiKey ) ) { throw new InvalidAPIKeyError ( ) ; return ; } schemaUtils . validateSchema ( req . body , schemas . metaTransactionFillRequestSchema ) ; const { mtx , signature } = parsePostTransactionRequestBody ( req ) ; const mtxHash = this . _metaTransactionService . getTransactionHash ( mtx ) ; const transactionInDatabase = await this . _metaTransactionService . findTransactionByHashAsync ( mtxHash ) ; if ( transactionInDatabase !== undefined ) { res . status ( HttpStatus . OK ) . send ( marshallTransactionEntity ( transactionInDatabase ) ) ; return ; } try { await this . _metaTransactionService . validateTransactionIsFillableAsync ( mtx , signature ) ; const ethTx = await this . _metaTransactionService . generatePartialExecuteTransactionEthereumTransactionAsync ( mtx , signature ) ; if ( apiKey !== undefined && MetaTransactionService . isEligibleForFreeMetaTxn ( apiKey ) ) { const isLive = await this . _metaTransactionService . isSignerLiveAsync ( ) ; if ( ! isLive ) { res . status ( HttpStatus . NOT_FOUND ) . send ( { code : APIErrorCodes . ServiceDisabled , reason : apiErrorCodesToReasons [ APIErrorCodes . ServiceDisabled ] , } ) ; return ; } if ( this . _rateLimiter !== undefined ) { const rateLimitResponse = await this . _rateLimiter . isAllowedAsync ( { apiKey , takerAddress : mtx . signer , } ) ; if ( isRateLimitedMetaTransactionResponse ( rateLimitResponse ) ) { res . status ( HttpStatus . TOO_MANY_REQUESTS ) . send ( { code : APIErrorCodes . UnableToSubmitOnBehalfOfTaker , reason : rateLimitResponse . reason , ethereumTransaction : { data : ethTx . data , gasPrice : ethTx . gasPrice , gas : ethTx . gas , value : ethTx . value , to : ethTx . to , } , } ) ; return ; } } const result = await this . _metaTransactionService . submitTransactionAsync ( mtxHash , mtx , signature , apiKey , affiliateAddress ) ; res . status ( HttpStatus . OK ) . send ( result ) ; } else { res . status ( HttpStatus . FORBIDDEN ) . send ( { code : APIErrorCodes . UnableToSubmitOnBehalfOfTaker , reason : apiErrorCodesToReasons [ APIErrorCodes . UnableToSubmitOnBehalfOfTaker ] , ethereumTransaction : { data : ethTx . data , gasPrice : ethTx . gasPrice , gas : ethTx . gas , value : ethTx . value , to : ethTx . to , } , } ) ; } } catch ( e ) { if ( isAPIError ( e ) ) { throw e ; } if ( isRevertError ( e ) ) { throw new RevertAPIError ( e ) ; } logger . info ( 'Uncaught error' , e ) ; throw new InternalServerError ( e . message ) ; } } public async getTransactionStatusAsync ( req , res ) { const transactionHash = req . params . txHash ; try { assert . isHexString ( 'transactionHash' , transactionHash ) ; } catch ( e ) { throw new ValidationError ( [ { field : 'txHash' , code : ValidationErrorCodes . InvalidSignatureOrHash , reason : e . message , } , ] ) ; } const tx = await this . _metaTransactionService . findTransactionByHashAsync ( transactionHash ) ; if ( tx === undefined ) { throw new NotFoundError ( ) ; } else { res . status ( HttpStatus . OK ) . send ( marshallTransactionEntity ( tx ) ) ; } } public async getSignerStatusAsync ( _req , res ) { try { const isLive = await this . _metaTransactionService . isSignerLiveAsync ( ) ; res . status ( HttpStatus . OK ) . send ( { isLive } ) ; } catch ( e ) { logger . error ( 'Uncaught error: ' , e ) ; throw new InternalServerError ( 'failed to check signer status' ) ; } } } const parseGetTransactionRequestParams = ( req ) : GetTransactionRequestParams => { const takerAddress = req . query . takerAddress as string ; const sellToken = req . query . sellToken as string ; const buyToken = req . query . buyToken as string ; const sellTokenAddress = findTokenAddressOrThrowApiError ( sellToken , 'sellToken' , CHAIN_ID ) ; const buyTokenAddress = findTokenAddressOrThrowApiError ( buyToken , 'buyToken' , CHAIN_ID ) ; 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 slippagePercentage = parseFloat ( req . query . slippagePercentage as string ) || DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE ; if ( slippagePercentage > 1 ) { throw new ValidationError ( [ { field : 'slippagePercentage' , code : ValidationErrorCodes . ValueOutOfRange , reason : ValidationErrorReasons . PercentageOutOfRange , } , ] ) ; } const _excludedSources = req . query . excludedSources === undefined ? [ ] : parseUtils . parseStringArrForERC20BridgeSources ( ( req . query . excludedSources as string ) . split ( ',' ) ) ; const includePriceComparisons = req . query . includePriceComparisons === 'true' ? true : false ; const includedSources = req . query . includedSources === undefined ? undefined : parseUtils . parseStringArrForERC20BridgeSources ( ( req . query . includedSources as string ) . split ( ',' ) ) ; const bntAddress = getTokenMetadataIfExists ( 'bnt' , ChainId . Mainnet ) ! . tokenAddress ; const isBNT = sellTokenAddress . toLowerCase ( ) === bntAddress || buyTokenAddress . toLowerCase ( ) === bntAddress ; const excludedSources = isBNT ? _excludedSources : _excludedSources . concat ( ERC20BridgeSource . Bancor ) ; const affiliateFee = parseUtils . parseAffiliateFeeOptions ( req ) ; const affiliateAddress = req . query . affiliateAddress as string | undefined ; return { takerAddress , sellTokenAddress , buyTokenAddress , sellAmount , buyAmount , slippagePercentage , excludedSources , includedSources , includePriceComparisons , affiliateFee , affiliateAddress , } ; } ; interface PostTransactionRequestBody { mtx : ExchangeProxyMetaTransactionWithoutDomain ; signature : Signature ; } const parsePostTransactionRequestBody = ( req ) : PostTransactionRequestBody => { const requestBody = req . body ; const signature = requestBody . signature ; const mtx = { signer : requestBody . transaction . signer , sender : requestBody . transaction . sender , salt : new BigNumber ( requestBody . transaction . salt ) , expirationTimeSeconds : new BigNumber ( requestBody . transaction . expirationTimeSeconds ) , minGasPrice : new BigNumber ( requestBody . transaction . minGasPrice ) , maxGasPrice : new BigNumber ( requestBody . transaction . maxGasPrice ) , callData : requestBody . transaction . callData , value : new BigNumber ( requestBody . transaction . value ) , feeToken : requestBody . transaction . feeToken , feeAmount : new BigNumber ( requestBody . transaction . feeAmount ) , } ; return { mtx , signature , } ; } ; const marshallTransactionEntity = ( tx ) : GetMetaTransactionStatusResponse => { return { refHash : tx . refHash , hash : tx . txHash , status : tx . status , gasPrice : tx . gasPrice , updatedAt : tx . updatedAt , blockNumber : tx . blockNumber , expectedMinedInSec : tx . expectedMinedInSec , ethereumTxStatus : _ . isNil ( tx . txStatus ) ? undefined : tx . txStatus , } ; } ;