From 2088d297315920ffc7f56bffcf59606873d7ccfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Wed, 3 Mar 2021 18:59:27 +0300 Subject: [PATCH 01/10] WIP --- src/index.ts | 2 + src/loadConfig.ts | 27 ++++- src/services/rates/RateEstimator.ts | 107 ++++++++++++------ src/services/rates/data.ts | 2 +- src/services/rates/index.ts | 5 +- .../rates/repo/impl/RateInfoLookup.ts | 27 +++-- .../rates/repo/impl/RemoteRateRepo.ts | 31 +++-- src/services/rates/repo/index.ts | 25 ++-- 8 files changed, 143 insertions(+), 83 deletions(-) diff --git a/src/index.ts b/src/index.ts index de27c2c8..1a74f0c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,8 @@ import * as notFoundHandler from './middleware/notFoundHandler'; import { loadConfig } from './loadConfig'; import router from './endpoints'; +export const WavesId: string = 'WAVES'; + const app = unsafeKoaQs(new Koa()); const options = loadConfig(); diff --git a/src/loadConfig.ts b/src/loadConfig.ts index be414a31..4a45c67b 100644 --- a/src/loadConfig.ts +++ b/src/loadConfig.ts @@ -26,12 +26,17 @@ export type MatcherConfig = { }; }; +export type RatesConfig = { + pairAcceptanceVolumeThreshold: number; +}; + export type DefaultConfig = PostgresConfig & ServerConfig & LoggerConfig; export type DataServiceConfig = PostgresConfig & ServerConfig & LoggerConfig & - MatcherConfig; + MatcherConfig & + RatesConfig; const commonEnvVariables = ['PGHOST', 'PGDATABASE', 'PGUSER', 'PGPASSWORD']; @@ -56,7 +61,15 @@ export const loadDefaultConfig = (): DefaultConfig => { }; }; -const envVariables = ['DEFAULT_MATCHER']; +const envVariables = ['DEFAULT_MATCHER', 'RATE_PAIR_ACCEPTANCE_VOLUME_THRESHOLD']; + +const ensurePositiveNumber = (x: number, msg: string) => { + if (x > 0) { + return x; + } + + throw new Error(msg); +}; const load = (): DataServiceConfig => { // assert all necessary env vars are set @@ -75,9 +88,19 @@ const load = (): DataServiceConfig => { matcher.matcher.settingsURL = process.env.MATCHER_SETTINGS_URL; } + const volumeThreshold = ensurePositiveNumber( + parseInt(process.env.RATE_PAIR_ACCEPTANCE_VOLUME_THRESHOLD as string), + 'RATE_PAIR_ACCEPTANCE_VOLUME_THRESHOLD environment variable should be a positive integer' + ); + + const rate: RatesConfig = { + pairAcceptanceVolumeThreshold: volumeThreshold, + }; + return { ...loadDefaultConfig(), ...matcher, + ...rate, }; }; diff --git a/src/services/rates/RateEstimator.ts b/src/services/rates/RateEstimator.ts index 6dc91393..83afaec6 100644 --- a/src/services/rates/RateEstimator.ts +++ b/src/services/rates/RateEstimator.ts @@ -1,36 +1,38 @@ import { BigNumber } from '@waves/data-entities'; import { Task } from 'folktale/concurrency/task'; import { Maybe } from 'folktale/maybe'; +import { isNil } from 'ramda'; -import { tap } from '../../utils/tap'; -import { AssetIdsPair, RateMgetParams } from '../../types'; import { AppError, DbError, Timeout } from '../../errorHandling'; +import { AssetIdsPair, Pair, RateMgetParams } from '../../types'; +import { tap } from '../../utils/tap'; +import { isEmpty } from '../../utils/fp/maybeOps'; -import { partitionByPreCount, AsyncMget, RateCache } from './repo'; +import { PairsService } from '../pairs'; +import { RateWithPairIds } from '../rates'; +import { partitionByPreComputed, AsyncMget, RateCache } from './repo'; import { RateCacheKey } from './repo/impl/RateCache'; import RateInfoLookup from './repo/impl/RateInfoLookup'; -import { isEmpty } from '../../utils/fp/maybeOps'; -import { RateWithPairIds } from '../rates'; type ReqAndRes = { req: TReq; res: Maybe; }; +export type VolumeAwareRateInfo = RateWithPairIds & { volumeWaves: BigNumber }; + export default class RateEstimator implements - AsyncMget< - RateMgetParams, - ReqAndRes, - AppError - > { + AsyncMget, AppError> { constructor( private readonly cache: RateCache, private readonly remoteGet: AsyncMget< RateMgetParams, RateWithPairIds, DbError | Timeout - > + >, + private readonly pairs: PairsService, + private readonly pairAcceptanceVolumeThreshold: number ) {} mget( @@ -45,18 +47,18 @@ export default class RateEstimator matcher, }); - const cacheUnlessCached = (item: AssetIdsPair, rate: BigNumber) => { + const cacheUnlessCached = (item: VolumeAwareRateInfo) => { const key = getCacheKey(item); if (!this.cache.has(key)) { - this.cache.set(key, rate); + this.cache.set(key, item); } }; - const cacheAll = (items: Array) => - items.forEach(it => cacheUnlessCached(it, it.rate)); + const cacheAll = (items: Array) => + items.forEach((it) => cacheUnlessCached(it)); - const { preCount, toBeRequested } = partitionByPreCount( + const { preComputed, toBeRequested } = partitionByPreComputed( this.cache, pairs, getCacheKey, @@ -65,29 +67,60 @@ export default class RateEstimator return this.remoteGet .mget({ pairs: toBeRequested, matcher, timestamp }) - .map(results => { - if (shouldCache) cacheAll(results); - return results; - }) - .map(data => new RateInfoLookup(data.concat(preCount))) - .map(lookup => - pairs.map(idsPair => ({ - req: idsPair, - res: lookup.get(idsPair), - })) - ) - .map( - tap(data => - data.forEach(reqAndRes => - reqAndRes.res.map( - tap(res => { - if (shouldCache) { - cacheUnlessCached(reqAndRes.req, res.rate); - } - }) + .chain((pairsWithRates) => + this.pairs + .mget({ + pairs: pairsWithRates, + matcher: request.matcher, + }) + .map((foundPairs) => + foundPairs.data.map((pair: Pair, idx) => { + if (isNil(pair.data)) { + return { + ...pairsWithRates[idx], + volumeWaves: new BigNumber(0), + }; + } else { + return { + amountAsset: pair.amountAsset as string, + priceAsset: pair.priceAsset as string, + volumeWaves: pair.data.volumeWaves, + rate: pairsWithRates[idx].rate, + }; + } + }) + ) + .map( + tap((results) => { + if (shouldCache) cacheAll(results); + }) + ) + .map( + (data) => + new RateInfoLookup( + data.concat(preComputed), + this.pairAcceptanceVolumeThreshold + ) + ) + .map((lookup) => + pairs.map((idsPair) => ({ + req: idsPair, + res: lookup.get(idsPair), + })) + ) + .map( + tap((data) => + data.forEach((reqAndRes) => + reqAndRes.res.map( + tap((res) => { + if (shouldCache) { + cacheUnlessCached(res); + } + }) + ) + ) ) ) - ) ); } } diff --git a/src/services/rates/data.ts b/src/services/rates/data.ts index ff79ecec..f92c182e 100644 --- a/src/services/rates/data.ts +++ b/src/services/rates/data.ts @@ -39,5 +39,5 @@ export function generatePossibleRequestItems( priceAsset: WavesId, }; - return [wavesL, flip(wavesL), wavesR, flip(wavesR)]; + return [wavesL, flip(wavesL), wavesR, flip(wavesR), pair, flip(pair)]; } diff --git a/src/services/rates/index.ts b/src/services/rates/index.ts index db4702a2..1e499701 100644 --- a/src/services/rates/index.ts +++ b/src/services/rates/index.ts @@ -19,8 +19,11 @@ export type RateWithPairIds = RateInfo & AssetIdsPair; export default function ({ drivers, cache, + pairs, + pairAcceptanceVolumeThreshold, }: RateSerivceCreatorDependencies): ServiceMget { - const estimator = new RateEstimator(cache, new RemoteRateRepo(drivers.pg)); + const estimator = new RateEstimator(cache, new RemoteRateRepo(drivers.pg),pairs, + pairAcceptanceVolumeThreshold,); return { mget(request: RateMgetParams) { diff --git a/src/services/rates/repo/impl/RateInfoLookup.ts b/src/services/rates/repo/impl/RateInfoLookup.ts index fba241cf..4b4b6ddf 100644 --- a/src/services/rates/repo/impl/RateInfoLookup.ts +++ b/src/services/rates/repo/impl/RateInfoLookup.ts @@ -7,11 +7,12 @@ import { WavesId, flip, pairHasWaves } from '../../data'; import { inv, safeDivide } from '../../util'; import { isDefined, map2 } from '../../../../utils/fp/maybeOps'; -import { RateWithPairIds } from '../../../rates' +import { RateWithPairIds } from '../../../rates'; +import { VolumeAwareRateInfo } from '../../../rates/RateEstimator'; type RateLookupTable = { [amountAsset: string]: { - [priceAsset: string]: BigNumber; + [priceAsset: string]: VolumeAwareRateInfo; }; }; @@ -26,7 +27,10 @@ export default class RateInfoLookup implements Omit, 'set'> { private readonly lookupTable: RateLookupTable; - constructor(data: Array) { + constructor( + data: Array, + private readonly pairAcceptanceVolumeThreshold: number + ) { this.lookupTable = this.toLookupTable(data); } @@ -34,16 +38,21 @@ export default class RateInfoLookup return isDefined(this.get(pair)); } - get(pair: AssetIdsPair): Maybe { + get(pair: AssetIdsPair): Maybe { const lookup = (pair: AssetIdsPair, flipped: boolean) => this.getFromLookupTable(pair, flipped); + if (pairHasWaves(pair)) { + return lookup(pair, false).orElse(() => lookup(pair, true)); + } + return lookup(pair, false) .orElse(() => lookup(pair, true)) + .filter((val) => val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold)) .orElse(() => maybeOf(pair) .filter(complement(pairHasWaves)) - .chain(pair => this.lookupThroughWaves(pair)) + .chain((pair) => this.lookupThroughWaves(pair)) ); } @@ -62,15 +71,13 @@ export default class RateInfoLookup private getFromLookupTable( pair: AssetIdsPair, flipped: boolean - ): Maybe { + ): Maybe { const lookupData = flipped ? flip(pair) : pair; return fromNullable( path([lookupData.amountAsset, lookupData.priceAsset], this.lookupTable) ) - .map((rate: BigNumber) => - flipped ? inv(rate).getOrElse(new BigNumber(0)) : rate - ) + .map((rate: BigNumber) => (flipped ? inv(rate).getOrElse(new BigNumber(0)) : rate)) .map((rate: BigNumber) => ({ rate, ...lookupData, @@ -88,7 +95,7 @@ export default class RateInfoLookup amountAsset: pair.priceAsset, priceAsset: WavesId, }) - ).map(rate => ({ + ).map((rate) => ({ amountAsset: pair.amountAsset, priceAsset: pair.priceAsset, rate: rate.getOrElse(new BigNumber(0)), diff --git a/src/services/rates/repo/impl/RemoteRateRepo.ts b/src/services/rates/repo/impl/RemoteRateRepo.ts index 9cb56028..bb409745 100644 --- a/src/services/rates/repo/impl/RemoteRateRepo.ts +++ b/src/services/rates/repo/impl/RemoteRateRepo.ts @@ -1,5 +1,5 @@ import * as knex from 'knex'; -import { chain, map } from 'ramda'; +import { chain } from 'ramda'; import { Task, of as taskOf } from 'folktale/concurrency/task'; import { DbError, Timeout } from '../../../../errorHandling'; @@ -15,13 +15,8 @@ export default class RemoteRateRepo implements AsyncMget { constructor(private readonly dbDriver: PgDriver) {} - mget( - request: RateMgetParams - ): Task> { - const pairsSqlParams = chain( - it => [it.amountAsset, it.priceAsset], - request.pairs - ); + mget(request: RateMgetParams): Task> { + const pairsSqlParams = chain((it) => [it.amountAsset, it.priceAsset], request.pairs); const sql = pg.raw(makeSql(request.pairs.length), [ request.timestamp.getOrElse(new Date()), @@ -30,19 +25,19 @@ export default class RemoteRateRepo ]); const dbTask: Task = - request.pairs.length === 0 - ? taskOf([]) - : this.dbDriver.any(sql.toString()); + request.pairs.length === 0 ? taskOf([]) : this.dbDriver.any(sql.toString()); return dbTask.map( (result: any[]): Array => - map((it: any): RateWithPairIds => { - return { - amountAsset: it.amount_asset_id, - priceAsset: it.price_asset_id, - rate: it.weighted_average_price, - }; - }, result) + result.map( + (it: any): RateWithPairIds => { + return { + amountAsset: it.amount_asset_id, + priceAsset: it.price_asset_id, + rate: it.weighted_average_price, + }; + } + ) ); } } diff --git a/src/services/rates/repo/index.ts b/src/services/rates/repo/index.ts index 93c20a69..7bdf358f 100644 --- a/src/services/rates/repo/index.ts +++ b/src/services/rates/repo/index.ts @@ -7,57 +7,54 @@ import { generatePossibleRequestItems, } from '../data'; import { RateCacheKey } from './impl/RateCache'; +import { VolumeAwareRateInfo } from '../RateEstimator'; import { Task } from 'folktale/concurrency/task'; import { RateWithPairIds } from '../../rates'; -export type RateCache = CacheSync; +export type RateCache = CacheSync; export type AsyncMget = { mget(req: Req): Task; }; export type PairsForRequest = { - preCount: RateWithPairIds[]; + preComputed: VolumeAwareRateInfo[]; toBeRequested: AssetIdsPair[]; }; -export const partitionByPreCount = ( +export const partitionByPreComputed = ( cache: RateCache, pairs: AssetIdsPair[], getCacheKey: (pair: AssetIdsPair) => RateCacheKey, - shouldCache: boolean + shouldCache: boolean, ): PairsForRequest => { const [eq, uneq] = partition(pairIsSymmetric, pairs); - const eqRates: Array = eq.map(pair => ({ + const eqRates: Array = eq.map((pair) => ({ rate: new BigNumber(1), ...pair, })); const allPairsToRequest = uniqWith( pairsEq, - chain(it => generatePossibleRequestItems(it), uneq) + chain((it) => generatePossibleRequestItems(it), uneq) ); if (shouldCache) { const [cached, uncached] = partition( - it => cache.has(getCacheKey(it)), + (it) => cache.has(getCacheKey(it)), allPairsToRequest ); - const cachedRates: Array = cached.map(pair => ({ - amountAsset: pair.amountAsset, - priceAsset: pair.priceAsset, - rate: cache.get(getCacheKey(pair)).getOrElse(new BigNumber(0)), - })); + const cachedRates = cached.map((pair) => cache.get(getCacheKey(pair)).unsafeGet()); return { - preCount: cachedRates.concat(eqRates), + preComputed: cachedRates.concat(eqRates), toBeRequested: uncached, }; } else { return { - preCount: eqRates, + preComputed: eqRates, toBeRequested: allPairsToRequest, }; } From c1e8b5255b7e1efbd90411f2af508f05714736fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Wed, 3 Mar 2021 20:04:06 +0300 Subject: [PATCH 02/10] add pair acceptence waves volume threashold, and update rate estimation according to it --- src/services/index.ts | 26 +++++----- src/services/rates/RateEstimator.ts | 5 +- src/services/rates/repo/impl/RateCache.ts | 11 ++--- .../rates/repo/impl/RateInfoLookup.ts | 47 ++++++++++--------- src/services/rates/repo/index.ts | 12 ++--- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/services/index.ts b/src/services/index.ts index 6c93fdb8..9710aefd 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -60,6 +60,8 @@ export type CommonServiceDependencies = { export type RateSerivceCreatorDependencies = CommonServiceDependencies & { cache: RateCache; + pairs: PairsService; + pairAcceptanceVolumeThreshold: number, }; export type ServiceMesh = { @@ -130,6 +132,17 @@ export default ({ cache: assetsCache, }); + const pairsNoAsyncValidation = createPairsService({ + ...commonDeps, + cache: pairsCache, + validatePairs: () => taskOf(undefined), + }); + const pairsWithAsyncValidation = createPairsService({ + ...commonDeps, + cache: pairsCache, + validatePairs: validatePairs(assets, pairOrderingService), + }); + const aliasTxs = createAliasTxsService(commonDeps); const burnTxs = createBurnTxsService(commonDeps); const dataTxs = createDataTxsService(commonDeps); @@ -150,17 +163,8 @@ export default ({ const rates = createRateService({ ...commonDeps, cache: ratesCache, - }); - - const pairsNoAsyncValidation = createPairsService({ - ...commonDeps, - cache: pairsCache, - validatePairs: () => taskOf(undefined), - }); - const pairsWithAsyncValidation = createPairsService({ - ...commonDeps, - cache: pairsCache, - validatePairs: validatePairs(assets, pairOrderingService), + pairs: pairsNoAsyncValidation, + pairAcceptanceVolumeThreshold: options.pairAcceptanceVolumeThreshold, }); const candlesNoAsyncValidation = createCandlesService({ diff --git a/src/services/rates/RateEstimator.ts b/src/services/rates/RateEstimator.ts index 83afaec6..a411acc4 100644 --- a/src/services/rates/RateEstimator.ts +++ b/src/services/rates/RateEstimator.ts @@ -70,7 +70,10 @@ export default class RateEstimator .chain((pairsWithRates) => this.pairs .mget({ - pairs: pairsWithRates, + pairs: pairsWithRates.map((pairWithRate) => ({ + amountAsset: pairWithRate.amountAsset, + priceAsset: pairWithRate.priceAsset, + })), matcher: request.matcher, }) .map((foundPairs) => diff --git a/src/services/rates/repo/impl/RateCache.ts b/src/services/rates/repo/impl/RateCache.ts index 6b556ee5..b50d1fe2 100644 --- a/src/services/rates/repo/impl/RateCache.ts +++ b/src/services/rates/repo/impl/RateCache.ts @@ -1,11 +1,10 @@ -import { BigNumber } from '@waves/data-entities'; import { fromNullable } from 'folktale/maybe'; import * as LRU from 'lru-cache'; import { AssetIdsPair } from '../../../../types'; -import { inv } from '../../util'; import { flip } from '../../data'; import { RateCache } from '../../repo'; +import { VolumeAwareRateInfo } from '../../RateEstimator'; export type RateCacheKey = { pair: AssetIdsPair; @@ -17,7 +16,7 @@ const keyFn = (matcher: string) => (pair: AssetIdsPair): string => { }; export default class RateCacheImpl implements RateCache { - private readonly lru: LRU; + private readonly lru: LRU; constructor(size: number, maxAgeMillis: number) { this.lru = new LRU({ max: size, maxAge: maxAgeMillis }); @@ -30,15 +29,15 @@ export default class RateCacheImpl implements RateCache { ); } - set(key: RateCacheKey, rate: BigNumber) { - this.lru.set(keyFn(key.matcher)(key.pair), rate); + set(key: RateCacheKey, data: VolumeAwareRateInfo) { + this.lru.set(keyFn(key.matcher)(key.pair), data); } get(key: RateCacheKey) { const getKey = keyFn(key.matcher); return fromNullable(this.lru.get(getKey(key.pair))).orElse(() => - fromNullable(this.lru.get(getKey(flip(key.pair)))).chain(inv) + fromNullable(this.lru.get(getKey(flip(key.pair)))) ); } } diff --git a/src/services/rates/repo/impl/RateInfoLookup.ts b/src/services/rates/repo/impl/RateInfoLookup.ts index 4b4b6ddf..6bf452ed 100644 --- a/src/services/rates/repo/impl/RateInfoLookup.ts +++ b/src/services/rates/repo/impl/RateInfoLookup.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@waves/data-entities'; -import { Maybe, of as maybeOf, fromNullable } from 'folktale/maybe'; -import { path, complement } from 'ramda'; +import { Maybe, fromNullable } from 'folktale/maybe'; +import { path } from 'ramda'; import { AssetIdsPair, CacheSync } from '../../../../types'; import { WavesId, flip, pairHasWaves } from '../../data'; @@ -49,20 +49,16 @@ export default class RateInfoLookup return lookup(pair, false) .orElse(() => lookup(pair, true)) .filter((val) => val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold)) - .orElse(() => - maybeOf(pair) - .filter(complement(pairHasWaves)) - .chain((pair) => this.lookupThroughWaves(pair)) - ); + .orElse(() => this.lookupThroughWaves(pair)); } - private toLookupTable(data: Array): RateLookupTable { + private toLookupTable(data: Array): RateLookupTable { return data.reduce((acc, item) => { if (!(item.amountAsset in acc)) { acc[item.amountAsset] = {}; } - acc[item.amountAsset][item.priceAsset] = item.rate; + acc[item.amountAsset][item.priceAsset] = item; return acc; }, {}); @@ -74,19 +70,28 @@ export default class RateInfoLookup ): Maybe { const lookupData = flipped ? flip(pair) : pair; - return fromNullable( + let foundValue = fromNullable( path([lookupData.amountAsset, lookupData.priceAsset], this.lookupTable) - ) - .map((rate: BigNumber) => (flipped ? inv(rate).getOrElse(new BigNumber(0)) : rate)) - .map((rate: BigNumber) => ({ - rate, - ...lookupData, - })); + ); + + return foundValue.map((data) => { + if (flipped) { + let flippedData = { ...data }; + flippedData.rate = inv(flippedData.rate).getOrElse(new BigNumber(0)); + return flippedData; + } else { + return data; + } + }); } - private lookupThroughWaves(pair: AssetIdsPair): Maybe { + private lookupThroughWaves(pair: AssetIdsPair): Maybe { return map2( - (info1, info2) => safeDivide(info1.rate, info2.rate), + (info1, info2) => ({ + ...pair, + rate: safeDivide(info1.rate, info2.rate).getOrElse(new BigNumber(0)), + volumeWaves: BigNumber.max(info1.volumeWaves, info2.volumeWaves), + }), this.get({ amountAsset: pair.amountAsset, priceAsset: WavesId, @@ -95,10 +100,6 @@ export default class RateInfoLookup amountAsset: pair.priceAsset, priceAsset: WavesId, }) - ).map((rate) => ({ - amountAsset: pair.amountAsset, - priceAsset: pair.priceAsset, - rate: rate.getOrElse(new BigNumber(0)), - })); + ); } } diff --git a/src/services/rates/repo/index.ts b/src/services/rates/repo/index.ts index 7bdf358f..3d698022 100644 --- a/src/services/rates/repo/index.ts +++ b/src/services/rates/repo/index.ts @@ -1,15 +1,10 @@ import { partition, chain, uniqWith } from 'ramda'; import { AssetIdsPair, CacheSync } from '../../../types'; import { BigNumber } from '@waves/data-entities'; -import { - pairIsSymmetric, - pairsEq, - generatePossibleRequestItems, -} from '../data'; +import { pairIsSymmetric, pairsEq, generatePossibleRequestItems } from '../data'; import { RateCacheKey } from './impl/RateCache'; import { VolumeAwareRateInfo } from '../RateEstimator'; import { Task } from 'folktale/concurrency/task'; -import { RateWithPairIds } from '../../rates'; export type RateCache = CacheSync; @@ -26,12 +21,13 @@ export const partitionByPreComputed = ( cache: RateCache, pairs: AssetIdsPair[], getCacheKey: (pair: AssetIdsPair) => RateCacheKey, - shouldCache: boolean, + shouldCache: boolean ): PairsForRequest => { const [eq, uneq] = partition(pairIsSymmetric, pairs); - const eqRates: Array = eq.map((pair) => ({ + const eqRates: Array = eq.map((pair) => ({ rate: new BigNumber(1), + volumeWaves: new BigNumber(0), ...pair, })); From 7a0f9694bb5edf3917e1f1dbc0a1754c8bbc9951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Tue, 23 Mar 2021 18:08:41 +0300 Subject: [PATCH 03/10] handle missing case in rate estimation --- src/services/rates/RateEstimator.ts | 2 +- src/services/rates/index.ts | 37 +++++++------- .../rates/repo/impl/RateInfoLookup.ts | 49 +++++++++++++------ .../rates/repo/impl/RemoteRateRepo.ts | 3 +- src/services/rates/repo/index.ts | 8 +-- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/services/rates/RateEstimator.ts b/src/services/rates/RateEstimator.ts index a411acc4..f7c1ad82 100644 --- a/src/services/rates/RateEstimator.ts +++ b/src/services/rates/RateEstimator.ts @@ -77,7 +77,7 @@ export default class RateEstimator matcher: request.matcher, }) .map((foundPairs) => - foundPairs.data.map((pair: Pair, idx) => { + foundPairs.data.map((pair: Pair, idx: number) => { if (isNil(pair.data)) { return { ...pairsWithRates[idx], diff --git a/src/services/rates/index.ts b/src/services/rates/index.ts index 1e499701..0a3aad70 100644 --- a/src/services/rates/index.ts +++ b/src/services/rates/index.ts @@ -1,20 +1,13 @@ import { BigNumber } from '@waves/data-entities'; -import { - ServiceMget, - Rate, - RateMgetParams, - list, - rate, - RateInfo, - AssetIdsPair, -} from '../../types'; +import { Maybe } from 'folktale/maybe'; +import { ServiceMget, Rate, RateMgetParams, list, rate, AssetIdsPair } from '../../types'; import { RateSerivceCreatorDependencies } from '../../services'; import RateEstimator from './RateEstimator'; import RemoteRateRepo from './repo/impl/RemoteRateRepo'; export { default as RateCacheImpl } from './repo/impl/RateCache'; -export type RateWithPairIds = RateInfo & AssetIdsPair; +export type RateWithPairIds = { rate: Maybe } & AssetIdsPair; export default function ({ drivers, @@ -22,8 +15,12 @@ export default function ({ pairs, pairAcceptanceVolumeThreshold, }: RateSerivceCreatorDependencies): ServiceMget { - const estimator = new RateEstimator(cache, new RemoteRateRepo(drivers.pg),pairs, - pairAcceptanceVolumeThreshold,); + const estimator = new RateEstimator( + cache, + new RemoteRateRepo(drivers.pg), + pairs, + pairAcceptanceVolumeThreshold + ); return { mget(request: RateMgetParams) { @@ -32,12 +29,16 @@ export default function ({ .map((data) => data.map((item) => rate( - { - rate: item.res.fold( - () => new BigNumber(0), - (it) => it.rate - ), - }, + item.res.fold( + () => null, + (it) => + it.rate.matchWith({ + Just: ({ value }) => ({ + rate: value, + }), + Nothing: () => null, + }) + ), { amountAsset: item.req.amountAsset, priceAsset: item.req.priceAsset, diff --git a/src/services/rates/repo/impl/RateInfoLookup.ts b/src/services/rates/repo/impl/RateInfoLookup.ts index 6bf452ed..6be68b25 100644 --- a/src/services/rates/repo/impl/RateInfoLookup.ts +++ b/src/services/rates/repo/impl/RateInfoLookup.ts @@ -1,11 +1,11 @@ import { BigNumber } from '@waves/data-entities'; -import { Maybe, fromNullable } from 'folktale/maybe'; +import { Maybe, fromNullable, of as maybeOf } from 'folktale/maybe'; import { path } from 'ramda'; import { AssetIdsPair, CacheSync } from '../../../../types'; import { WavesId, flip, pairHasWaves } from '../../data'; import { inv, safeDivide } from '../../util'; -import { isDefined, map2 } from '../../../../utils/fp/maybeOps'; +import { isDefined } from '../../../../utils/fp/maybeOps'; import { RateWithPairIds } from '../../../rates'; import { VolumeAwareRateInfo } from '../../../rates/RateEstimator'; @@ -46,10 +46,23 @@ export default class RateInfoLookup return lookup(pair, false).orElse(() => lookup(pair, true)); } + let wavesPaired = this.lookupThroughWaves(pair); + return lookup(pair, false) .orElse(() => lookup(pair, true)) - .filter((val) => val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold)) - .orElse(() => this.lookupThroughWaves(pair)); + .filter( + (val) => + val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold) || + wavesPaired.matchWith({ + Just: ({ value }) => + value.rate.matchWith({ + Just: () => false, + Nothing: () => true, + }), + Nothing: () => true, + }) + ) + .orElse(() => wavesPaired); } private toLookupTable(data: Array): RateLookupTable { @@ -77,7 +90,7 @@ export default class RateInfoLookup return foundValue.map((data) => { if (flipped) { let flippedData = { ...data }; - flippedData.rate = inv(flippedData.rate).getOrElse(new BigNumber(0)); + flippedData.rate = flippedData.rate.chain((rate) => inv(rate)); return flippedData; } else { return data; @@ -86,20 +99,24 @@ export default class RateInfoLookup } private lookupThroughWaves(pair: AssetIdsPair): Maybe { - return map2( - (info1, info2) => ({ - ...pair, - rate: safeDivide(info1.rate, info2.rate).getOrElse(new BigNumber(0)), - volumeWaves: BigNumber.max(info1.volumeWaves, info2.volumeWaves), - }), - this.get({ - amountAsset: pair.amountAsset, - priceAsset: WavesId, - }), + return this.get({ + amountAsset: pair.amountAsset, + priceAsset: WavesId, + }).chain((info1) => this.get({ amountAsset: pair.priceAsset, priceAsset: WavesId, - }) + }).chain((info2) => + info1.rate.chain((rate1) => + info2.rate.chain((rate2) => + maybeOf({ + ...pair, + rate: safeDivide(rate1, rate2), + volumeWaves: BigNumber.max(info1.volumeWaves, info2.volumeWaves), + }) + ) + ) + ) ); } } diff --git a/src/services/rates/repo/impl/RemoteRateRepo.ts b/src/services/rates/repo/impl/RemoteRateRepo.ts index bb409745..2876f62d 100644 --- a/src/services/rates/repo/impl/RemoteRateRepo.ts +++ b/src/services/rates/repo/impl/RemoteRateRepo.ts @@ -1,5 +1,6 @@ import * as knex from 'knex'; import { chain } from 'ramda'; +import { fromNullable } from 'folktale/maybe'; import { Task, of as taskOf } from 'folktale/concurrency/task'; import { DbError, Timeout } from '../../../../errorHandling'; @@ -34,7 +35,7 @@ export default class RemoteRateRepo return { amountAsset: it.amount_asset_id, priceAsset: it.price_asset_id, - rate: it.weighted_average_price, + rate: fromNullable(it.weighted_average_price), }; } ) diff --git a/src/services/rates/repo/index.ts b/src/services/rates/repo/index.ts index 3d698022..fbf1de64 100644 --- a/src/services/rates/repo/index.ts +++ b/src/services/rates/repo/index.ts @@ -1,10 +1,12 @@ import { partition, chain, uniqWith } from 'ramda'; -import { AssetIdsPair, CacheSync } from '../../../types'; import { BigNumber } from '@waves/data-entities'; +import { of as maybeOf } from 'folktale/maybe'; +import { Task } from 'folktale/concurrency/task'; + +import { AssetIdsPair, CacheSync } from '../../../types'; import { pairIsSymmetric, pairsEq, generatePossibleRequestItems } from '../data'; import { RateCacheKey } from './impl/RateCache'; import { VolumeAwareRateInfo } from '../RateEstimator'; -import { Task } from 'folktale/concurrency/task'; export type RateCache = CacheSync; @@ -26,7 +28,7 @@ export const partitionByPreComputed = ( const [eq, uneq] = partition(pairIsSymmetric, pairs); const eqRates: Array = eq.map((pair) => ({ - rate: new BigNumber(1), + rate: maybeOf(new BigNumber(1)), volumeWaves: new BigNumber(0), ...pair, })); From 1048671f4ac5f01418853ca5a58042a47e0aabb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Wed, 7 Apr 2021 15:46:32 +0300 Subject: [PATCH 04/10] rate pair acceptance volume threshold in custom asset --- src/loadConfig.ts | 6 ++++-- src/services/index.ts | 5 +++++ src/services/rates/RateEstimator.ts | 16 +++++++++------- src/services/rates/index.ts | 6 ++++-- src/services/rates/repo/impl/RateInfoLookup.ts | 5 ++--- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/loadConfig.ts b/src/loadConfig.ts index 4a45c67b..9a614b8a 100644 --- a/src/loadConfig.ts +++ b/src/loadConfig.ts @@ -28,6 +28,7 @@ export type MatcherConfig = { export type RatesConfig = { pairAcceptanceVolumeThreshold: number; + thresholdAssetId: string; }; export type DefaultConfig = PostgresConfig & ServerConfig & LoggerConfig; @@ -54,14 +55,14 @@ export const loadDefaultConfig = (): DefaultConfig => { postgresPoolSize: process.env.PGPOOLSIZE ? parseInt(process.env.PGPOOLSIZE) : 20, postgresStatementTimeout: isNil(process.env.PGSTATEMENTTIMEOUT) || - isNaN(parseInt(process.env.PGSTATEMENTTIMEOUT)) + isNaN(parseInt(process.env.PGSTATEMENTTIMEOUT)) ? false : parseInt(process.env.PGSTATEMENTTIMEOUT), logLevel: process.env.LOG_LEVEL || 'info', }; }; -const envVariables = ['DEFAULT_MATCHER', 'RATE_PAIR_ACCEPTANCE_VOLUME_THRESHOLD']; +const envVariables = ['DEFAULT_MATCHER', 'RATE_PAIR_ACCEPTANCE_VOLUME_THRESHOLD', 'RATE_THRESHOLD_ASSET_ID']; const ensurePositiveNumber = (x: number, msg: string) => { if (x > 0) { @@ -95,6 +96,7 @@ const load = (): DataServiceConfig => { const rate: RatesConfig = { pairAcceptanceVolumeThreshold: volumeThreshold, + thresholdAssetId: process.env.RATE_THRESHOLD_ASSET_ID as string }; return { diff --git a/src/services/index.ts b/src/services/index.ts index 9710aefd..c4f17a2a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -41,6 +41,7 @@ import createTransferTxsService, { TransferTxsService } from './transactions/tra import createUpdateAssetInfoTxsService, { UpdateAssetInfoTxsService } from './transactions/updateAssetInfo'; import { DataServiceConfig } from '../loadConfig'; import createRateService, { RateCacheImpl } from './rates'; +import { IThresholdAssetRateService, ThresholdAssetRateService } from './rates/ThresholdAssetRateService'; import { PairOrderingServiceImpl } from './PairOrderingService'; @@ -62,6 +63,7 @@ export type RateSerivceCreatorDependencies = CommonServiceDependencies & { cache: RateCache; pairs: PairsService; pairAcceptanceVolumeThreshold: number, + thresholdAssetRateService: IThresholdAssetRateService }; export type ServiceMesh = { @@ -143,6 +145,8 @@ export default ({ validatePairs: validatePairs(assets, pairOrderingService), }); + const thresholdAssetRateService = new ThresholdAssetRateService(options.thresholdAssetId, options.matcher.defaultMatcherAddress, pairsNoAsyncValidation); + const aliasTxs = createAliasTxsService(commonDeps); const burnTxs = createBurnTxsService(commonDeps); const dataTxs = createDataTxsService(commonDeps); @@ -165,6 +169,7 @@ export default ({ cache: ratesCache, pairs: pairsNoAsyncValidation, pairAcceptanceVolumeThreshold: options.pairAcceptanceVolumeThreshold, + thresholdAssetRateService: thresholdAssetRateService, }); const candlesNoAsyncValidation = createCandlesService({ diff --git a/src/services/rates/RateEstimator.ts b/src/services/rates/RateEstimator.ts index f7c1ad82..2bb934d7 100644 --- a/src/services/rates/RateEstimator.ts +++ b/src/services/rates/RateEstimator.ts @@ -10,6 +10,7 @@ import { isEmpty } from '../../utils/fp/maybeOps'; import { PairsService } from '../pairs'; import { RateWithPairIds } from '../rates'; +import { IThresholdAssetRateService } from './ThresholdAssetRateService'; import { partitionByPreComputed, AsyncMget, RateCache } from './repo'; import { RateCacheKey } from './repo/impl/RateCache'; import RateInfoLookup from './repo/impl/RateInfoLookup'; @@ -23,7 +24,7 @@ export type VolumeAwareRateInfo = RateWithPairIds & { volumeWaves: BigNumber }; export default class RateEstimator implements - AsyncMget, AppError> { + AsyncMget, AppError> { constructor( private readonly cache: RateCache, private readonly remoteGet: AsyncMget< @@ -32,8 +33,9 @@ export default class RateEstimator DbError | Timeout >, private readonly pairs: PairsService, - private readonly pairAcceptanceVolumeThreshold: number - ) {} + private readonly pairAcceptanceVolumeThreshold: number, + private readonly thresholdAssetRateService: IThresholdAssetRateService + ) { } mget( request: RateMgetParams @@ -98,12 +100,12 @@ export default class RateEstimator if (shouldCache) cacheAll(results); }) ) - .map( + .chain( (data) => - new RateInfoLookup( + this.thresholdAssetRateService.get().map(thresholdAssetRate => new RateInfoLookup( data.concat(preComputed), - this.pairAcceptanceVolumeThreshold - ) + new BigNumber(this.pairAcceptanceVolumeThreshold).dividedBy(thresholdAssetRate), + )) ) .map((lookup) => pairs.map((idsPair) => ({ diff --git a/src/services/rates/index.ts b/src/services/rates/index.ts index 0a3aad70..54ea8017 100644 --- a/src/services/rates/index.ts +++ b/src/services/rates/index.ts @@ -1,10 +1,10 @@ import { BigNumber } from '@waves/data-entities'; import { Maybe } from 'folktale/maybe'; + import { ServiceMget, Rate, RateMgetParams, list, rate, AssetIdsPair } from '../../types'; import { RateSerivceCreatorDependencies } from '../../services'; import RateEstimator from './RateEstimator'; import RemoteRateRepo from './repo/impl/RemoteRateRepo'; - export { default as RateCacheImpl } from './repo/impl/RateCache'; export type RateWithPairIds = { rate: Maybe } & AssetIdsPair; @@ -14,12 +14,14 @@ export default function ({ cache, pairs, pairAcceptanceVolumeThreshold, + thresholdAssetRateService }: RateSerivceCreatorDependencies): ServiceMget { const estimator = new RateEstimator( cache, new RemoteRateRepo(drivers.pg), pairs, - pairAcceptanceVolumeThreshold + pairAcceptanceVolumeThreshold, + thresholdAssetRateService ); return { diff --git a/src/services/rates/repo/impl/RateInfoLookup.ts b/src/services/rates/repo/impl/RateInfoLookup.ts index 6be68b25..27f13356 100644 --- a/src/services/rates/repo/impl/RateInfoLookup.ts +++ b/src/services/rates/repo/impl/RateInfoLookup.ts @@ -29,7 +29,7 @@ export default class RateInfoLookup constructor( data: Array, - private readonly pairAcceptanceVolumeThreshold: number + private readonly pairAcceptanceVolumeThreshold: BigNumber ) { this.lookupTable = this.toLookupTable(data); } @@ -51,8 +51,7 @@ export default class RateInfoLookup return lookup(pair, false) .orElse(() => lookup(pair, true)) .filter( - (val) => - val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold) || + (val) => val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold) || wavesPaired.matchWith({ Just: ({ value }) => value.rate.matchWith({ From 9d77ca485417c12a9a4cc991c1d53a1f3a0792b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Wed, 7 Apr 2021 15:47:03 +0300 Subject: [PATCH 05/10] rate pair acceptance volume threshold in custom asset: add missing module --- .../rates/ThresholdAssetRateService.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/services/rates/ThresholdAssetRateService.ts diff --git a/src/services/rates/ThresholdAssetRateService.ts b/src/services/rates/ThresholdAssetRateService.ts new file mode 100644 index 00000000..376a7026 --- /dev/null +++ b/src/services/rates/ThresholdAssetRateService.ts @@ -0,0 +1,47 @@ +import * as LRU from 'lru-cache'; +import { BigNumber } from "@waves/data-entities"; +import { Task, of as taskOf, rejected } from 'folktale/concurrency/task'; + +import { AppError } from "../../errorHandling"; +import { WavesId } from "../.."; +import { PairsService } from "../pairs"; + +export interface IThresholdAssetRateService { + get(): Task +}; + +export class ThresholdAssetRateService implements IThresholdAssetRateService { + private cache: LRU; + + constructor(private readonly thresholdAssetId: string, private readonly matcherAddress: string, private readonly pairsService: PairsService) { + this.cache = new LRU({ maxAge: 60000 }); + } + + get(): Task { + let rate = this.cache.get(this.thresholdAssetId); + if (rate === undefined) { + // rate was not set or is stale + return this.pairsService.get({ + pair: { + amountAsset: WavesId, + priceAsset: this.thresholdAssetId, + }, matcher: this.matcherAddress + }).chain(m => { + return m.matchWith({ + Just: ({ value }) => { + if (value.data === null) { + return rejected(AppError.Resolver(`Rate for pair WAVES/${this.thresholdAssetId} not found`)); + } + this.cache.set(this.thresholdAssetId, value.data.weightedAveragePrice); + return taskOf(value.data?.weightedAveragePrice) + }, + Nothing: () => { + return rejected(AppError.Resolver(`Pair WAVES/${this.thresholdAssetId} not found`)); + } + }) + }); + } else { + return taskOf(rate); + } + } +} \ No newline at end of file From 218ce4642eb2547b0beb53817174a8127d45f916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Wed, 7 Apr 2021 16:56:50 +0300 Subject: [PATCH 06/10] minor ts fix --- src/services/rates/ThresholdAssetRateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/rates/ThresholdAssetRateService.ts b/src/services/rates/ThresholdAssetRateService.ts index 376a7026..c3843f6d 100644 --- a/src/services/rates/ThresholdAssetRateService.ts +++ b/src/services/rates/ThresholdAssetRateService.ts @@ -33,7 +33,7 @@ export class ThresholdAssetRateService implements IThresholdAssetRateService { return rejected(AppError.Resolver(`Rate for pair WAVES/${this.thresholdAssetId} not found`)); } this.cache.set(this.thresholdAssetId, value.data.weightedAveragePrice); - return taskOf(value.data?.weightedAveragePrice) + return taskOf(value.data.weightedAveragePrice); }, Nothing: () => { return rejected(AppError.Resolver(`Pair WAVES/${this.thresholdAssetId} not found`)); From cafff5266cc26a6d6541c8754661fac0bd8d3b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Thu, 8 Apr 2021 12:47:13 +0300 Subject: [PATCH 07/10] revert rate response structure --- src/services/rates/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/services/rates/index.ts b/src/services/rates/index.ts index 54ea8017..19390634 100644 --- a/src/services/rates/index.ts +++ b/src/services/rates/index.ts @@ -31,16 +31,16 @@ export default function ({ .map((data) => data.map((item) => rate( - item.res.fold( - () => null, - (it) => - it.rate.matchWith({ - Just: ({ value }) => ({ - rate: value, - }), - Nothing: () => null, - }) - ), + { + rate: item.res.fold( + () => new BigNumber(0), + (it) => + it.rate.matchWith({ + Just: ({ value }) => value, + Nothing: () => new BigNumber(0), + }) + ), + }, { amountAsset: item.req.amountAsset, priceAsset: item.req.priceAsset, From b41efbf05e2645b404462bb2279e109d5dbd55b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Thu, 8 Apr 2021 13:23:31 +0300 Subject: [PATCH 08/10] 0.31.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cce38f24..f87c429d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "data-service", - "version": "0.30.0", + "version": "0.31.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0c2dee85..f31130dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-service", - "version": "0.30.0", + "version": "0.31.3", "description": "Waves data service", "main": "src/index.js", "repository": "git@github.com:wavesplatform/data-service.git", From 1d9a2237eb3b7a172d97b5566180171d04bf01f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Thu, 8 Apr 2021 21:10:01 +0300 Subject: [PATCH 09/10] fixed --- src/services/rates/RateEstimator.ts | 4 ++-- src/services/rates/repo/impl/RateInfoLookup.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/rates/RateEstimator.ts b/src/services/rates/RateEstimator.ts index 2bb934d7..0787fd23 100644 --- a/src/services/rates/RateEstimator.ts +++ b/src/services/rates/RateEstimator.ts @@ -20,7 +20,7 @@ type ReqAndRes = { res: Maybe; }; -export type VolumeAwareRateInfo = RateWithPairIds & { volumeWaves: BigNumber }; +export type VolumeAwareRateInfo = RateWithPairIds & { volumeWaves: BigNumber | null }; export default class RateEstimator implements @@ -101,7 +101,7 @@ export default class RateEstimator }) ) .chain( - (data) => + (data: Array<{ amountAsset: string, priceAsset: string, volumeWaves: BigNumber | null, rate: Maybe }>) => this.thresholdAssetRateService.get().map(thresholdAssetRate => new RateInfoLookup( data.concat(preComputed), new BigNumber(this.pairAcceptanceVolumeThreshold).dividedBy(thresholdAssetRate), diff --git a/src/services/rates/repo/impl/RateInfoLookup.ts b/src/services/rates/repo/impl/RateInfoLookup.ts index 27f13356..546d10b7 100644 --- a/src/services/rates/repo/impl/RateInfoLookup.ts +++ b/src/services/rates/repo/impl/RateInfoLookup.ts @@ -51,7 +51,7 @@ export default class RateInfoLookup return lookup(pair, false) .orElse(() => lookup(pair, true)) .filter( - (val) => val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold) || + (val) => (val.volumeWaves !== null && val.volumeWaves.gte(this.pairAcceptanceVolumeThreshold)) || wavesPaired.matchWith({ Just: ({ value }) => value.rate.matchWith({ @@ -111,7 +111,7 @@ export default class RateInfoLookup maybeOf({ ...pair, rate: safeDivide(rate1, rate2), - volumeWaves: BigNumber.max(info1.volumeWaves, info2.volumeWaves), + volumeWaves: BigNumber.max(info1.volumeWaves || 0, info2.volumeWaves || 0), }) ) ) From d0df1a44a3b3b9b58f1c6796c66235bbf93e7597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90leksej=20Petrov?= Date: Thu, 8 Apr 2021 21:25:23 +0300 Subject: [PATCH 10/10] 0.31.4 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f87c429d..cfad9ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "data-service", - "version": "0.31.3", + "version": "0.31.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f31130dc..3ac4c441 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-service", - "version": "0.31.3", + "version": "0.31.4", "description": "Waves data service", "main": "src/index.js", "repository": "git@github.com:wavesplatform/data-service.git",