diff --git a/package.json b/package.json index be41539ff..37605d1aa 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "generate:types": "yarn generate:types:midgard && yarn generate:types:thornode", "generate:types:midgard": "yarn clean:types:midgard && TS_POST_PROCESS_FILE=./node_modules/.bin/prettier openapi-generator-cli generate -i https://midgard.ninerealms.com/v2/swagger.json -g typescript-rxjs -o ./src/renderer/types/generated/midgard --reserved-words-mappings in=in --enable-post-process-file", "clean:types:midgard": "rimraf ./src/renderer/types/generated/midgard", - "generate:types:thornode": "yarn clean:types:thornode && TS_POST_PROCESS_FILE=./node_modules/.bin/prettier openapi-generator-cli generate -i https://gitlab.com/thorchain/thornode/-/raw/6e10252deb6d65775c484b9fe8c5814195be44be/openapi/openapi.yaml -g typescript-rxjs -o ./src/renderer/types/generated/thornode --enable-post-process-file --skip-validate-spec", + "generate:types:thornode": "yarn clean:types:thornode && TS_POST_PROCESS_FILE=./node_modules/.bin/prettier openapi-generator-cli generate -i https://gitlab.com/thorchain/thornode/-/raw/653029bc41dd928b702237552d29e10d094c7ad4/openapi/openapi.yaml -g typescript-rxjs -o ./src/renderer/types/generated/thornode --enable-post-process-file --skip-validate-spec", "clean:types:thornode": "rimraf ./src/renderer/types/generated/thornode", "storybook": "sb dev -p 9009 --no-manager-cache", "build-storybook": "sb build", diff --git a/src/renderer/helpers/fp/eq.ts b/src/renderer/helpers/fp/eq.ts index e28ce6382..316489e9b 100644 --- a/src/renderer/helpers/fp/eq.ts +++ b/src/renderer/helpers/fp/eq.ts @@ -97,6 +97,8 @@ export const eqApiError = Eq.struct({ msg: eqString }) +export const eqError: Eq.Eq = { equals: (x, y) => x === y } + export const eqBalances = A.getEq(eqBalance) export const eqAssetsWithAmount = A.getEq(eqAssetWithAmount) diff --git a/src/renderer/helpers/savers.ts b/src/renderer/helpers/savers.ts index 622688bbc..4425980cc 100644 --- a/src/renderer/helpers/savers.ts +++ b/src/renderer/helpers/savers.ts @@ -1,5 +1,5 @@ import { getValueOfAsset1InAsset2, PoolData } from '@thorchain/asgardex-util' -import { assetFromString, BaseAmount, baseAmount } from '@xchainjs/xchain-util' +import { assetFromString, BaseAmount, baseAmount, bnOrZero } from '@xchainjs/xchain-util' import * as A from 'fp-ts/lib/Array' import * as FP from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' @@ -28,12 +28,13 @@ export const getSaversTableRowData = ({ poolDetail, pricePoolData, watchlist, + maxSynthPerPoolDepth, network }: { - // TODO(@veado) Update Midgard types - poolDetail: PoolDetail & { saversDepth?: string } + poolDetail: PoolDetail pricePoolData: PoolData watchlist: PoolsWatchList + maxSynthPerPoolDepth: number network: Network }): O.Option => { return FP.pipe( @@ -44,6 +45,13 @@ export const getSaversTableRowData = ({ const poolData = toPoolData(poolDetail) const depthAmount = baseAmount(poolDetail.saversDepth) const depthPrice = getValueOfAsset1InAsset2(depthAmount, poolData, pricePoolData) + const apr = bnOrZero(poolDetail.saversAPR).times(100) + + const maxPercent = bnOrZero(maxSynthPerPoolDepth).div(100) + // saverCap = assetDepth * 2 * maxPercent / 100 + const saverCap = bnOrZero(poolDetail.assetDepth).times(2).times(maxPercent).div(100) + // filled = saversDepth * 100 / saverCap + const filled = bnOrZero(poolDetail.saversDepth).times(100).div(saverCap) const watched: boolean = FP.pipe( watchlist, @@ -55,11 +63,10 @@ export const getSaversTableRowData = ({ asset: poolDetailAsset, depth: depthAmount, depthPrice, - filled: 0, // TODO(@veado) Get dat from extra data - count: 0, // TODO(@veado) Get dat from extra data + filled, key: poolDetailAsset.ticker, network, - apr: 0, // get APR + apr, watched } }) @@ -70,11 +77,13 @@ export const getSaversTableRowsData = ({ poolDetails, pricePoolData, watchlist, + maxSynthPerPoolDepth, network }: { poolDetails: PoolDetails pricePoolData: PoolData watchlist: PoolsWatchList + maxSynthPerPoolDepth: number network: Network }): SaversTableRowsData => { // get symbol of deepest pool @@ -99,7 +108,7 @@ export const getSaversTableRowsData = ({ ) return FP.pipe( - getSaversTableRowData({ poolDetail, pricePoolData, watchlist, network }), + getSaversTableRowData({ poolDetail, pricePoolData, watchlist, maxSynthPerPoolDepth, network }), O.map( (poolTableRowData) => ({ diff --git a/src/renderer/hooks/usePoolCycle.ts b/src/renderer/hooks/usePoolCycle.ts index 0caf61716..ace28761b 100644 --- a/src/renderer/hooks/usePoolCycle.ts +++ b/src/renderer/hooks/usePoolCycle.ts @@ -22,14 +22,13 @@ export const usePoolCycle = (): { poolCycle: PoolCycleRD reloadPoolCycle: FP.Lazy } => { - const { mimir$, reloadMimir } = useThorchainContext() - const { thorchainConstantsState$, reloadThorchainConstants } = useThorchainContext() + const { mimir$, reloadMimir, thorchainConstantsState$, reloadThorchainConstants } = useThorchainContext() - const midgardConstantsPoolCycle$: PoolCycleLD = useMemo( + const tnConstantsPoolCycle$: PoolCycleLD = useMemo( () => FP.pipe( thorchainConstantsState$, - liveData.map(({ int64_values }) => Number(int64_values?.PoolCycle)), + liveData.map(({ int_64_values }) => Number(int_64_values?.PoolCycle)), liveData.chain((poolCycle) => // validation -> value needs to be a number liveData.fromPredicate( @@ -37,7 +36,7 @@ export const usePoolCycle = (): { () => Error(`Invalid value of constant 'PoolCycle' ${poolCycle} `) )(poolCycle) ), - liveData.mapLeft(() => ({ errorId: ErrorId.GET_POOL_CYCLE, msg: 'Unable to get constant of PoolCycle' })) + liveData.mapLeft(() => ({ errorId: ErrorId.GET_TC_CONSTANT, msg: 'Unable to get constant of PoolCycle' })) ), [thorchainConstantsState$] ) @@ -48,7 +47,7 @@ export const usePoolCycle = (): { mimir$, liveData.map(({ POOLCYCLE: poolCycle }) => O.fromNullable(poolCycle)), liveData.chain(liveData.fromOption(() => Error('Unable to load pool cycle from Mimir'))), - liveData.mapLeft(({ message }) => ({ errorId: ErrorId.GET_POOL_CYCLE, msg: message })) + liveData.mapLeft(({ message }) => ({ errorId: ErrorId.GET_TC_CONSTANT, msg: message })) ), [mimir$] ) @@ -61,11 +60,11 @@ export const usePoolCycle = (): { const [data] = useObservableState( () => FP.pipe( - Rx.combineLatest([midgardConstantsPoolCycle$, mimirPoolCycle$]), - RxOp.map(([mimirPoolCycleRD, midgardPoolCycleRD]) => + Rx.combineLatest([tnConstantsPoolCycle$, mimirPoolCycle$]), + RxOp.map(([tnPoolCycleRD, mimirPoolCycleRD]) => FP.pipe( mimirPoolCycleRD, - RD.alt(() => midgardPoolCycleRD) + RD.alt(() => tnPoolCycleRD) ) ), RxOp.distinctUntilChanged(eqPoolCycle.equals) diff --git a/src/renderer/hooks/useSynthConstants.ts b/src/renderer/hooks/useSynthConstants.ts new file mode 100644 index 000000000..c450559ed --- /dev/null +++ b/src/renderer/hooks/useSynthConstants.ts @@ -0,0 +1,79 @@ +import { useCallback, useEffect, useMemo } from 'react' + +import * as RD from '@devexperts/remote-data-ts' +import * as FP from 'fp-ts/function' +import * as N from 'fp-ts/lib/number' +import * as O from 'fp-ts/lib/Option' +import { useObservableState } from 'observable-hooks' +import * as Rx from 'rxjs' +import * as RxOp from 'rxjs/operators' + +import { useThorchainContext } from '../contexts/ThorchainContext' +import { eqError } from '../helpers/fp/eq' +import { LiveData, liveData } from '../helpers/rx/liveData' + +const eqMaxSynthPerPoolDepthRD = RD.getEq(eqError, N.Eq) + +type MaxSynthPerPoolDepthRD = RD.RemoteData +type MaxSynthPerPoolDepthLD = LiveData + +export const useSynthConstants = (): { + maxSynthPerPoolDepth: MaxSynthPerPoolDepthRD + reloadConstants: FP.Lazy +} => { + const { mimir$, reloadMimir, thorchainConstantsState$, reloadThorchainConstants } = useThorchainContext() + + const tnMaxSynthPerPoolDepth$: MaxSynthPerPoolDepthLD = useMemo( + () => + FP.pipe( + thorchainConstantsState$, + liveData.map(({ int_64_values }) => Number(int_64_values?.MaxSynthPerPoolDepth)), + liveData.chain((value) => + // validation -> value needs to be a number + liveData.fromPredicate( + () => !isNaN(value), + () => Error(`Invalid value of constant 'MaxSynthPerPoolDepth' ${value} `) + )(value) + ) + ), + [thorchainConstantsState$] + ) + + const mimirMaxSynthPerPoolDepth$: MaxSynthPerPoolDepthLD = useMemo( + () => + FP.pipe( + mimir$, + liveData.map(({ MAXSYNTHPERPOOLDEPTH: poolCycle }) => O.fromNullable(poolCycle)), + liveData.chain(liveData.fromOption(() => Error(`Unable to get 'MAXSYNTHPERPOOLDEPTH' from Mimir`))) + ), + [mimir$] + ) + + const getData = useCallback(() => { + reloadMimir() + reloadThorchainConstants() + }, [reloadThorchainConstants, reloadMimir]) + + const [data] = useObservableState( + () => + FP.pipe( + Rx.combineLatest([mimirMaxSynthPerPoolDepth$, tnMaxSynthPerPoolDepth$]), + RxOp.map(([mimirMaxSynthPerPoolDepthRD, tnMaxSynthPerPoolDepthRD]) => + FP.pipe( + mimirMaxSynthPerPoolDepthRD, + RD.alt(() => tnMaxSynthPerPoolDepthRD) + ) + ), + RxOp.distinctUntilChanged(eqMaxSynthPerPoolDepthRD.equals) + ), + RD.initial + ) + + // Reload data on mount + useEffect(() => { + getData() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return { maxSynthPerPoolDepth: data, reloadConstants: getData } +} diff --git a/src/renderer/services/thorchain/types.ts b/src/renderer/services/thorchain/types.ts index d274b7471..15f94a786 100644 --- a/src/renderer/services/thorchain/types.ts +++ b/src/renderer/services/thorchain/types.ts @@ -134,6 +134,7 @@ export type ThornodeApiUrlRD = RD.RemoteData export const MimirIO = t.type({ MAXIMUMLIQUIDITYRUNE: t.union([t.number, t.undefined]), POOLCYCLE: t.union([t.number, t.undefined]), + MAXSYNTHPERPOOLDEPTH: t.union([t.number, t.undefined]), HALTTRADING: t.union([t.number, t.undefined]), HALTTHORCHAIN: t.union([t.number, t.undefined]), HALTETHCHAIN: t.union([t.number, t.undefined]), diff --git a/src/renderer/services/wallet/types.ts b/src/renderer/services/wallet/types.ts index 1c3d913bb..0405e19dd 100644 --- a/src/renderer/services/wallet/types.ts +++ b/src/renderer/services/wallet/types.ts @@ -172,7 +172,7 @@ export enum ErrorId { VALIDATE_NODE = 'VALIDATE_NODE', VALIDATE_RESULT = 'VALIDATE_RESULT', GET_ACTIONS = 'GET_ACTIONS', - GET_POOL_CYCLE = 'GET_POOL_CYCLE' + GET_TC_CONSTANT = 'GET_TC_CONSTANT' } export type ChainBalancesService = { diff --git a/src/renderer/types/generated/thornode/models/ConstantsResponse.ts b/src/renderer/types/generated/thornode/models/ConstantsResponse.ts index 00bc41130..1641dc655 100644 --- a/src/renderer/types/generated/thornode/models/ConstantsResponse.ts +++ b/src/renderer/types/generated/thornode/models/ConstantsResponse.ts @@ -20,7 +20,7 @@ export interface ConstantsResponse { * @type {{ [key: string]: string; }} * @memberof ConstantsResponse */ - int64_values?: { [key: string]: string; }; + int_64_values?: { [key: string]: string; }; /** * @type {{ [key: string]: string; }} * @memberof ConstantsResponse diff --git a/src/renderer/views/savers/Savers.types.ts b/src/renderer/views/savers/Savers.types.ts index b1df828a1..03ac954b3 100644 --- a/src/renderer/views/savers/Savers.types.ts +++ b/src/renderer/views/savers/Savers.types.ts @@ -1,4 +1,5 @@ import { BaseAmount, Asset } from '@xchainjs/xchain-util' +import BigNumber from 'bignumber.js' import { Network } from '../../../shared/api/types' @@ -6,9 +7,8 @@ export type SaversTableRowData = { asset: Asset depthPrice: BaseAmount depth: BaseAmount - filled: number - count: number - apr: number + filled: BigNumber + apr: BigNumber key: string network: Network watched: boolean diff --git a/src/renderer/views/savers/SaversOverview.tsx b/src/renderer/views/savers/SaversOverview.tsx index b725cb4e0..acc62d16e 100644 --- a/src/renderer/views/savers/SaversOverview.tsx +++ b/src/renderer/views/savers/SaversOverview.tsx @@ -8,10 +8,12 @@ import { BaseAmount, baseToAsset, Chain, - formatAssetAmountCurrency + formatAssetAmountCurrency, + formatBN } from '@xchainjs/xchain-util' import { Grid } from 'antd' import { ColumnsType, ColumnType } from 'antd/lib/table' +import BigNumber from 'bignumber.js' import * as A from 'fp-ts/lib/Array' import * as FP from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' @@ -23,11 +25,13 @@ import { FlatButton } from '../../components/uielements/button' import { Table } from '../../components/uielements/table' import { useMidgardContext } from '../../contexts/MidgardContext' import { isChainAsset } from '../../helpers/assetHelper' -import { ordNumber } from '../../helpers/fp/ord' +import { ordBigNumber } from '../../helpers/fp/ord' +import { sequenceTRD } from '../../helpers/fpHelpers' import * as PoolHelpers from '../../helpers/poolHelper' import { getSaversTableRowsData, ordSaversByDepth } from '../../helpers/savers' import { useNetwork } from '../../hooks/useNetwork' import { usePoolWatchlist } from '../../hooks/usePoolWatchlist' +import { useSynthConstants } from '../../hooks/useSynthConstants' import * as poolsRoutes from '../../routes/pools' import { PoolDetails, PoolsState } from '../../services/midgard/types' import type { MimirHalt } from '../../services/thorchain/types' @@ -52,9 +56,12 @@ export const SaversOverview: React.FC = (props): JSX.Element => { } } = useMidgardContext() + const { maxSynthPerPoolDepth: maxSynthPerPoolDepthRD, reloadConstants } = useSynthConstants() + const refreshHandler = useCallback(() => { reloadPools() - }, [reloadPools]) + reloadConstants() + }, [reloadConstants, reloadPools]) const selectedPricePool = useObservableState(selectedPricePool$, PoolHelpers.RUNE_PRICE_POOL) @@ -99,36 +106,24 @@ export const SaversOverview: React.FC = (props): JSX.Element => { ) const aprColumn = useCallback( - (): ColumnType => ({ + (): ColumnType => ({ key: 'apr', align: 'center', title: intl.formatMessage({ id: 'pools.apr' }), - render: ({ apr }: { apr: number }) =>
{apr}%
, - sorter: (a: { apr: number }, b: { apr: number }) => ordNumber.compare(a.apr, b.apr), + render: ({ apr }: { apr: BigNumber }) =>
{formatBN(apr, 2)}%
, + sorter: (a: { apr: BigNumber }, b: { apr: BigNumber }) => ordBigNumber.compare(a.apr, b.apr), sortDirections: ['descend', 'ascend'] }), [intl] ) const filledColumn = useCallback( - (): ColumnType => ({ + (): ColumnType => ({ key: 'filled', align: 'center', title: intl.formatMessage({ id: 'pools.filled' }), - render: ({ filled }: { filled: number }) =>
{filled}%
, - sorter: (a: { filled: number }, b: { filled: number }) => ordNumber.compare(a.filled, b.filled), - sortDirections: ['descend', 'ascend'] - }), - [intl] - ) - - const countColumn = useCallback( - (): ColumnType => ({ - key: 'count', - align: 'center', - title: intl.formatMessage({ id: 'pools.count' }), - render: ({ count }: { count: number }) =>
{count}
, - sorter: (a: { count: number }, b: { count: number }) => ordNumber.compare(a.count, b.count), + render: ({ filled }: { filled: BigNumber }) =>
{formatBN(filled, 2)}%
, + sorter: (a: { filled: BigNumber }, b: { filled: BigNumber }) => ordBigNumber.compare(a.filled, b.filled), sortDirections: ['descend', 'ascend'] }), [intl] @@ -187,7 +182,6 @@ export const SaversOverview: React.FC = (props): JSX.Element => { Shared.poolColumn(intl.formatMessage({ id: 'common.pool' })), Shared.assetColumn(intl.formatMessage({ id: 'common.asset' })), depthColumn(selectedPricePool.asset), - countColumn(), filledColumn(), aprColumn(), btnColumn() @@ -196,7 +190,6 @@ export const SaversOverview: React.FC = (props): JSX.Element => { addPoolToWatchlist, aprColumn, btnColumn, - countColumn, depthColumn, filledColumn, intl, @@ -241,42 +234,47 @@ export const SaversOverview: React.FC = (props): JSX.Element => { return ( <> - {RD.fold( - // initial state - () => renderTable([], true), - // loading state - () => { - const pools = O.getOrElse(() => [] as SaversTableRowsData)(previousSavers.current) - return renderTable(pools, true) - }, - // render error state - Shared.renderTableError(intl.formatMessage({ id: 'common.refresh' }), refreshHandler), - // success state - ({ poolDetails }: PoolsState): JSX.Element => { - // filter chain assets - const poolDetailsFiltered: PoolDetails = FP.pipe( - poolDetails, - A.filter(({ asset: assetString }) => - FP.pipe( - assetString, - assetFromString, - O.fromNullable, - O.map(isChainAsset), - O.getOrElse(() => false) + {FP.pipe( + sequenceTRD(poolsRD, maxSynthPerPoolDepthRD), + RD.fold( + // initial state + () => renderTable([], true), + // loading state + () => { + const pools = O.getOrElse(() => [] as SaversTableRowsData)(previousSavers.current) + return renderTable(pools, true) + }, + // render error state + Shared.renderTableError(intl.formatMessage({ id: 'common.refresh' }), refreshHandler), + // success state + ([pools, maxSynthPerPoolDepth]): JSX.Element => { + const { poolDetails }: PoolsState = pools + // filter chain assets + const poolDetailsFiltered: PoolDetails = FP.pipe( + poolDetails, + A.filter(({ asset: assetString }) => + FP.pipe( + assetString, + assetFromString, + O.fromNullable, + O.map(isChainAsset), + O.getOrElse(() => false) + ) ) ) - ) - const poolViewData = getSaversTableRowsData({ - poolDetails: poolDetailsFiltered, - pricePoolData: selectedPricePool.poolData, - watchlist: poolWatchList, - network - }) - previousSavers.current = O.some(poolViewData) - return renderTable(poolViewData) - } - )(poolsRD)} + const poolViewData = getSaversTableRowsData({ + poolDetails: poolDetailsFiltered, + pricePoolData: selectedPricePool.poolData, + watchlist: poolWatchList, + maxSynthPerPoolDepth, + network + }) + previousSavers.current = O.some(poolViewData) + return renderTable(poolViewData) + } + ) + )} ) }