diff --git a/package.json b/package.json index 00a04df..1e3939b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "@apollo/client": "^3.9.10", "@distributedlab/tools": "^1.0.0-rc.13", + "@ethersproject/abi": "^5.7.0", + "@ethersproject/providers": "^5.7.2", "@vuelidate/core": "^2.0.0", "@vuelidate/validators": "^2.0.0", "@vueuse/core": "^10.1.2", @@ -85,6 +87,6 @@ "yorkie": "^2.0.0" }, "engines": { - "node": "18" + "node": "22" } } diff --git a/src/common/InfoDashboard/helpers.ts b/src/common/InfoDashboard/helpers.ts index a481409..8351a5a 100644 --- a/src/common/InfoDashboard/helpers.ts +++ b/src/common/InfoDashboard/helpers.ts @@ -6,20 +6,21 @@ import { mapKeys, mapValues } from 'lodash' type ChartData = Record const ONE_DAY_TIMESTAMP = 24 * 60 * 60 +const DECIMAL = BigNumber.from(10).pow(25) export async function getChartData( poolId: number, poolStartedAt: BigNumber, month: number, ): Promise { - type QueryData = Record<`r${number}`, { totalStaked: string }[]> + type QueryData = Record<`r${number}`, { totalStaked?: string }[]> const { data } = await config.apolloClient.query({ query: _generateTotalStakedPerDayGraphqlQuery(poolId, poolStartedAt, month), }) return mapValues( mapKeys(data, (_, key) => key.slice(1)), - value => BigNumber.from(value[0].totalStaked), + value => BigNumber.from(value[0]?.totalStaked ?? 0), ) } @@ -67,3 +68,139 @@ function _generateTotalStakedPerDayGraphqlQuery( ${'{\n' + requests.join('\n') + '\n}'} ` } + +export async function getUserYieldPerDayChartData( + poolId: number, + user: string, + month: number, +): Promise { + type PoolIntercation = { + timestamp: string + rate: string + } + + type QueryData = { + userInteractions: { + timestamp: string + rate: string + deposited: string + claimedRewards: string + pendingRewards: string + }[] + poolInteractions: PoolIntercation[] + } + + // Get data from TheGraph + const { data } = await config.apolloClient.query({ + query: _generateUserYieldPerDayGraphqlQuery(poolId, user, month), + }) + + // START leave yields only at the end of the calculation day + const poolInteractionsMap = new Map() + data.poolInteractions.forEach(interaction => { + const { timestamp } = interaction + const date = new Date(Number(timestamp) * 1000) + const day = date.toISOString().split('T')[0] + + if ( + !poolInteractionsMap.has(day) || + Number(timestamp) > Number(poolInteractionsMap.get(day)!.timestamp) + ) { + poolInteractionsMap.set(day, interaction) + } + }) + const poolInteractions = Array.from(poolInteractionsMap.values()) + // END + + // START calculate rewards + const yields: ChartData = {} + for (let i = 0; i < data.userInteractions.length; i++) { + const ui = data.userInteractions[i] + const nextUserIntercation = + i < data.userInteractions.length - 1 + ? data.userInteractions[i + 1] + : undefined + + // Get `poolInteractions` periods between `userIntercationsYield` + // When `userInteraction` is last, get all periods that greater then current + const periodPoolInteractions = poolInteractions.filter(e => { + return ( + Number(e.timestamp) > Number(ui.timestamp) && + (nextUserIntercation + ? Number(e.timestamp) < Number(nextUserIntercation.timestamp) + : true) + ) + }) + + // Calculate current yield from the `userIntercations` and push + const uiv = BigNumber.from(ui.claimedRewards).add(ui.pendingRewards) + yields[Number(ui.timestamp)] = uiv + + // Calculate nex period yields from the `poolIntercations` and push + periodPoolInteractions.forEach(pi => { + const rateDiff = BigNumber.from(pi.rate).sub(ui.rate) + const periodReward = BigNumber.from(ui.deposited) + .mul(rateDiff) + .div(DECIMAL) + + const value = uiv.add(periodReward) + + yields[Number(pi.timestamp)] = value + }) + } + // END + + return yields +} + +function _generateUserYieldPerDayGraphqlQuery( + poolId: number, + user: string, + // TODO: add month + month: number, +) { + const fromTimestamp = + new Time(String(month + 1), 'M').toDate().getTime() / 1000 + const toTimestamp = new Time(String(month + 2), 'M').toDate().getTime() / 1000 + + const REQUEST_PATTERN = ` + userInteractions ( + orderBy: timestamp + orderDirection: asc + where: { + user: "${user}" + poolId: "${poolId.toString()}" + timestamp_gt: ${fromTimestamp} + timestamp_lt: ${toTimestamp} + } + ) { + timestamp + rate + deposited + claimedRewards + pendingRewards + } + poolInteractions ( + orderBy: timestamp + orderDirection: asc + where: { + rate_gt: 0 + timestamp_gt: ${fromTimestamp} + timestamp_lt: ${toTimestamp} + pool_: { + id: "${hexlify(poolId)}" + } + } + first: 1000 + ) { + timestamp + rate + } + ` + + const requests = [REQUEST_PATTERN] + + return gql` + ${'{\n' + requests.join('\n') + '\n}'} + ` +} diff --git a/src/common/InfoDashboard/index.vue b/src/common/InfoDashboard/index.vue index 44b56c4..3bd5652 100644 --- a/src/common/InfoDashboard/index.vue +++ b/src/common/InfoDashboard/index.vue @@ -3,28 +3,48 @@
-
-
-
- {{ $t('info-dashboard.header-title') }} -
- -
-

- {{ $t('info-dashboard.header-subtitle') }} -

+
+
+ {{ chartTitle }} +
+ +
+
+ +
-
+
+

+ {{ chartSubtitle }} +

+ +
+ chartType.value === CHART_TYPE.circulingSupply + ? t('info-dashboard.header-supply-title') + : t('info-dashboard.header-earned-title'), +) + +const chartSubtitle = computed(() => + chartType.value === CHART_TYPE.circulingSupply + ? t('info-dashboard.header-supply-subtitle') + : t('info-dashboard.header-earned-subtitle'), +) + const monthOptions = computed[]>(() => { const allMonthOptions = Array.from({ length: 12 }).map((_, idx) => ({ title: t(`months.${idx}`), @@ -131,26 +176,56 @@ const isChartDataUpdating = ref(false) const chartConfig = reactive({ ...CHART_CONFIG }) +const updateSupplyChartData = async (month: number) => { + const chartData = await getChartData( + props.poolId, + props.poolData!.payoutStart, + month, + ) + + const monthTime = new Time(String(month + 1), 'M') + + chartConfig.data.labels = Object.keys(chartData).map( + day => `${monthTime.format('MMMM')} ${day}`, + ) + chartConfig.data.datasets[0].data = Object.values(chartData).map(amount => + Math.ceil(Number(formatEther(amount))), + ) + + chartConfig.data.datasets[0].borderColor = + CHART_COLORS[CHART_TYPE.circulingSupply] + chartConfig.data.datasets[0].pointBackgroundColor = + CHART_COLORS[CHART_TYPE.circulingSupply] +} + +const updateEarnedMorChartData = async (month: number) => { + const chartData = await getUserYieldPerDayChartData( + props.poolId, + web3ProvidersStore.address, + month, + ) + + chartConfig.data.labels = Object.keys(chartData).map(timestamp => { + return new Time(Number(timestamp)).format('DD MMMM') + }) + chartConfig.data.datasets[0].data = Object.values(chartData).map(amount => + Number(formatEther(amount)), + ) + + chartConfig.data.datasets[0].borderColor = CHART_COLORS[CHART_TYPE.earnedMor] + chartConfig.data.datasets[0].pointBackgroundColor = + CHART_COLORS[CHART_TYPE.earnedMor] +} + const updateChartData = async (month: number) => { isChartDataUpdating.value = true try { if (!props.poolData) throw new Error('poolData unavailable') - const chartData = await getChartData( - props.poolId, - props.poolData.payoutStart, - month, - ) - - const monthTime = new Time(String(month + 1), 'M') - - chartConfig.data.labels = Object.keys(chartData).map( - day => `${monthTime.format('MMMM')} ${day}`, - ) - chartConfig.data.datasets[0].data = Object.values(chartData).map(amount => - Math.ceil(Number(formatEther(amount))), - ) + chartType.value === CHART_TYPE.circulingSupply + ? await updateSupplyChartData(month) + : await updateEarnedMorChartData(month) } catch (error) { ErrorHandler.process(error) } @@ -158,13 +233,20 @@ const updateChartData = async (month: number) => { isChartDataUpdating.value = false } +const changeChartType = (chartToSet: CHART_TYPE) => { + chartType.value = chartToSet +} + onMounted(() => { if (props.poolData) updateChartData(selectedMonth.value.value) }) -watch([selectedMonth, () => props.poolData], async ([newSelectedMonth]) => { - await updateChartData(newSelectedMonth.value) -}) +watch( + [selectedMonth, () => props.poolData, chartType], + async ([newSelectedMonth]) => { + await updateChartData(newSelectedMonth.value) + }, +)