import { BalancerFillData , CurveFillData , FillData , UniswapV2FillData } from '@0x/asset-swapper' ; import { DexSample , ERC20BridgeSource , MarketDepthSide , NativeFillData , } from '@0x/asset-swapper/lib/src/utils/market_operation_utils/types' ; import { MarketOperation } from '@0x/types' ; import { BigNumber } from '@0x/utils' ; import { Web3Wrapper } from '@0x/web3-wrapper' ; import _ = require ( 'lodash' ) ; import { MARKET_DEPTH_DEFAULT_DISTRIBUTION , MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC , MARKET_DEPTH_MAX_SAMPLES , ZERO , } from '../constants' ; import { BucketedPriceDepth , TokenMetadata } from '../types' ; const MAX_DECIMALS = 18 ; const ONE_HUNDRED_PERC = 100 ; export const marketDepthUtils = { getBucketPrices : ( startAmount , endAmount , numSamples , sampleDistributionBase = 1 ) : BigNumber [ ] => { const amount = endAmount . minus ( startAmount ) ; const distribution = [ ... Array < BigNumber > ( numSamples ) ] . map ( ( _v , i ) => new BigNumber ( sampleDistributionBase ) . pow ( i ) . decimalPlaces ( MAX_DECIMALS ) ) ; const stepSizes = distribution . map ( ( d ) => d . div ( BigNumber . sum ( ... distribution ) ) ) ; const amounts = stepSizes . map ( ( _s , i ) => { return amount . times ( BigNumber . sum ( ... [ 0 , ... stepSizes . slice ( 0 , i + 1 ) ] ) ) . plus ( startAmount ) . decimalPlaces ( MAX_DECIMALS ) ; } ) ; return [ startAmount , ... amounts ] ; } , calculateUnitPrice : ( input , output , outputToken , inputToken ) : BigNumber => { if ( output && input && output . isGreaterThan ( 0 ) ) { return Web3Wrapper . toUnitAmount ( output , outputToken . decimals ) . dividedBy ( Web3Wrapper . toUnitAmount ( input , inputToken . decimals ) ) ; } return ZERO ; } , getSampleAmountsFromDepthSide : ( depthSide ) : BigNumber [ ] => { const nativeIndexIfExists = depthSide . findIndex ( ( s ) => s [ 0 ] && s [ 0 ] . source === ERC20BridgeSource . Native ) ; const nonNativeIndexIfExists = depthSide . findIndex ( ( s ) => s [ 0 ] && s [ 0 ] . source !== ERC20BridgeSource . Native ) ; const sampleAmounts = nonNativeIndexIfExists !== - 1 ? depthSide [ nonNativeIndexIfExists ] . map ( ( s ) => s . input ) : _ . uniqBy < BigNumber > ( depthSide [ nativeIndexIfExists ] . map ( ( s ) => s . input ) , ( a ) => a . toString ( ) ) ; return sampleAmounts ; } , sampleNativeOrders : ( path , targetInput , side ) : BigNumber => { let sortedPath = path . sort ( ( a , b ) => b . output . dividedBy ( b . input ) . comparedTo ( a . output . dividedBy ( a . input ) ) ) ; sortedPath = side === MarketOperation . Sell ? sortedPath : sortedPath . reverse ( ) ; let totalOutput = ZERO ; let totalInput = ZERO ; for ( const fill of sortedPath ) { if ( totalInput . gte ( targetInput ) ) { break ; } const input = BigNumber . min ( targetInput . minus ( totalInput ) , fill . input ) ; const output = input . times ( fill . output . dividedBy ( fill . input ) ) . integerValue ( ) ; totalOutput = totalOutput . plus ( output ) ; totalInput = totalInput . plus ( input ) ; } if ( totalInput . isLessThan ( targetInput ) ) { return ZERO ; } return totalOutput ; } , normalizeMarketDepthToSampleOutput : ( depthSide , side ) : MarketDepthSide => { const nativeIndexIfExists = depthSide . findIndex ( ( s ) => s [ 0 ] && s [ 0 ] . source === ERC20BridgeSource . Native && s [ 0 ] . output ) ; if ( nativeIndexIfExists === - 1 ) { return depthSide . filter ( ( s ) => s && s . length > 0 ) ; } const sampleAmounts = marketDepthUtils . getSampleAmountsFromDepthSide ( depthSide ) ; const nativeSamples = sampleAmounts . map ( ( a ) => { const sample = marketDepthUtils . sampleNativeOrders ( depthSide [ nativeIndexIfExists ] as DexSample < NativeFillData > [ ] , a , side ) ; const input = a ; const output = sample ; return { input , output , source : ERC20BridgeSource . Native , fillData : { } , } ; } ) ; const normalizedDepth = [ ... depthSide . filter ( ( s ) => s [ 0 ] && s [ 0 ] . source !== ERC20BridgeSource . Native ) , nativeSamples , ] . filter ( ( s ) => s . length > 0 ) ; return normalizedDepth ; } , calculateStartEndBucketPrice : ( depthSide , side , endSlippagePerc = 20 ) : [ BigNumber , BigNumber ] => { const pricesByAmount = depthSide . map ( ( samples ) => samples . map ( ( s ) => ( ! s . output . isZero ( ) ? s . output . dividedBy ( s . input ) . decimalPlaces ( MAX_DECIMALS ) : ZERO ) ) . filter ( ( s ) => s . isGreaterThan ( ZERO ) ) ) . filter ( ( samples ) => samples . length > 0 ) ; let bestInBracket ; let worstBestInBracket ; if ( side === MarketOperation . Sell ) { bestInBracket = BigNumber . max ( ... pricesByAmount . map ( ( s ) => BigNumber . max ( ... s ) ) ) ; worstBestInBracket = bestInBracket . times ( ( ONE_HUNDRED_PERC - endSlippagePerc ) / ONE_HUNDRED_PERC ) ; } else { bestInBracket = BigNumber . min ( ... pricesByAmount . map ( ( s ) => BigNumber . min ( ... s ) ) ) ; worstBestInBracket = bestInBracket . times ( ( ONE_HUNDRED_PERC + endSlippagePerc ) / ONE_HUNDRED_PERC ) ; } return [ bestInBracket , worstBestInBracket ] ; } , distributeSamplesToBuckets : ( depthSide , buckets , side ) => { const allocatedBuckets = buckets . map ( ( b , i ) => ( { price : b , bucket : i , bucketTotal : ZERO , sources : { } } ) ) ; const getBucketId = ( price ) : number => { return buckets . findIndex ( ( b ) => { return side === MarketOperation . Sell ? price . isGreaterThanOrEqualTo ( b ) : price . isLessThanOrEqualTo ( b ) ; } ) ; } ; const sampleToSourceKey = ( sample ) : string => { const source = sample . source ; if ( ! sample . fillData ) { return source ; } switch ( source ) { case ERC20BridgeSource . Curve : return `${ source }:${ ( sample . fillData as CurveFillData ) . pool . poolAddress }` ; case ERC20BridgeSource . Balancer : return `${ source }:${ ( sample . fillData as BalancerFillData ) . poolAddress }` ; case ERC20BridgeSource . UniswapV2 : return `${ source }:${ ( sample . fillData as UniswapV2FillData ) . tokenAddressPath . join ( '-' ) }` ; default : break ; } return source ; } ; for ( const samples of depthSide ) { const source = sampleToSourceKey ( samples [ 0 ] ) ; for ( const sample of samples ) { if ( sample . output . isZero ( ) ) { continue ; } const price = sample . output . dividedBy ( sample . input ) . decimalPlaces ( MAX_DECIMALS ) ; const bucketId = getBucketId ( price ) ; if ( bucketId === - 1 ) { continue ; } const bucket = allocatedBuckets [ bucketId ] ; bucket . bucketTotal = bucket . sources [ source ] && ! bucket . sources [ source ] . isZero ( ) ? bucket . bucketTotal . minus ( bucket . sources [ source ] ) . plus ( sample . input ) : bucket . bucketTotal . plus ( sample . input ) ; bucket . sources [ source ] = sample . input ; } } const normalizedCumulativeBuckets = allocatedBuckets . map ( ( b , bucketIndex ) => { const findLastNonEmptyBucketResult = ( source ) => { let lastValue = ZERO ; for ( let i = bucketIndex - 1 ; i >= 0 ; i -- ) { const value = allocatedBuckets [ i ] . sources [ source ] ; if ( value && ! value . isZero ( ) ) { lastValue = value ; break ; } } return lastValue ; } ; for ( const key of Object . keys ( b . sources ) ) { const source = key . split ( ':' ) [ 0 ] ; if ( source !== key && Object . values ( ERC20BridgeSource ) . includes ( source as ERC20BridgeSource ) ) { b . sources [ source ] = b . sources [ source ] ? b . sources [ source ] . plus ( b . sources [ key ] ) : b . sources [ key ] ; delete b . sources [ key ] ; } } for ( const source of Object . values ( ERC20BridgeSource ) ) { const previousValue = findLastNonEmptyBucketResult ( source ) ; b . sources [ source ] = BigNumber . max ( previousValue , b . sources [ source ] || ZERO ) ; } const cumulative = BigNumber . sum ( ... Object . values < BigNumber > ( b . sources ) ) ; return { ... b , cumulative , sources : b . sources } ; } ) ; return normalizedCumulativeBuckets ; } , calculateDepthForSide : ( rawDepthSide , side , numBuckets = MARKET_DEPTH_MAX_SAMPLES , bucketDistribution = MARKET_DEPTH_DEFAULT_DISTRIBUTION , maxEndSlippagePercentage = MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC ) : BucketedPriceDepth [ ] => { const depthSide = marketDepthUtils . normalizeMarketDepthToSampleOutput ( rawDepthSide , side ) ; const [ startPrice , endPrice ] = marketDepthUtils . calculateStartEndBucketPrice ( depthSide , side , maxEndSlippagePercentage ) ; const buckets = marketDepthUtils . getBucketPrices ( startPrice , endPrice , numBuckets , bucketDistribution ) ; const distributedBuckets = marketDepthUtils . distributeSamplesToBuckets ( depthSide , buckets , side ) ; return distributedBuckets ; } , } ;