diff --git a/frontend/app/src/comps/StakePositionSummary/StakePositionSummary.tsx b/frontend/app/src/comps/StakePositionSummary/StakePositionSummary.tsx index 6195120d0..f94b1dcf9 100644 --- a/frontend/app/src/comps/StakePositionSummary/StakePositionSummary.tsx +++ b/frontend/app/src/comps/StakePositionSummary/StakePositionSummary.tsx @@ -4,10 +4,12 @@ import { useAppear } from "@/src/anim-utils"; import { Amount } from "@/src/comps/Amount/Amount"; import { TagPreview } from "@/src/comps/TagPreview/TagPreview"; import { fmtnum } from "@/src/formatting"; +import { useGovernanceUser } from "@/src/subgraph-hooks"; import { css } from "@/styled-system/css"; -import { HFlex, IconStake, InfoTooltip, TokenIcon } from "@liquity2/uikit"; +import { HFlex, IconStake, InfoTooltip, TokenIcon, useRaf } from "@liquity2/uikit"; import { a } from "@react-spring/web"; import * as dn from "dnum"; +import { useRef } from "react"; export function StakePositionSummary({ loadingState = "success", @@ -20,7 +22,40 @@ export function StakePositionSummary({ stakePosition: null | PositionStake; txPreviewMode?: boolean; }) { - const appear = useAppear(loadingState === "success"); + const govUser = useGovernanceUser(stakePosition?.owner ?? null); + + const appear = useAppear(loadingState === "success" && govUser.status === "success"); + + // votingPower(t) = lqty * t - offset + const votingPower = (timestamp: bigint) => { + if (!govUser.data) { + return null; + } + return ( + BigInt(govUser.data.stakedLQTY) * timestamp + - BigInt(govUser.data.stakedOffset) + ); + }; + + const votingPowerRef = useRef(null); + useRaf(() => { + if (!votingPowerRef.current) { + return; + } + + const vp = votingPower(BigInt(Date.now())); + if (vp === null) { + votingPowerRef.current.innerHTML = "0"; + return; + } + + const vpAsNum = Number(vp / 10n ** 18n) / 1000 / 1000; + votingPowerRef.current.innerHTML = fmtnum( + vpAsNum, + { digits: 2, trailingZeros: true }, + ); + }, 60); + return (
-
- {prevStakePosition && stakePosition && !dn.eq(prevStakePosition.share, stakePosition.share) - ? ( -
- -
- ) - : " of pool"} + {prevStakePosition && stakePosition && !dn.eq(prevStakePosition.share, stakePosition.share) && ( +
+ +
+ )} - Voting power is the percentage of the total staked LQTY that you own. + Voting power is the total staked LQTY that you own.
It is calculated as:
+ lqty * t - offset
) diff --git a/frontend/app/src/graphql/gql.ts b/frontend/app/src/graphql/gql.ts index d36af04c8..9b0c5555c 100644 --- a/frontend/app/src/graphql/gql.ts +++ b/frontend/app/src/graphql/gql.ts @@ -28,6 +28,7 @@ const documents = { "\n query InterestBatch($id: ID!) {\n interestBatch(id: $id) {\n collateral {\n collIndex\n }\n batchManager\n debt\n coll\n annualInterestRate\n annualManagementFee\n }\n }\n": types.InterestBatchDocument, "\n query InterestRateBrackets($collId: String!) {\n interestRateBrackets(where: { collateral: $collId }, orderBy: rate) {\n rate\n totalDebt\n }\n }\n": types.InterestRateBracketsDocument, "\n query GovernanceInitiatives {\n governanceInitiatives {\n id\n }\n }\n": types.GovernanceInitiativesDocument, + "\n query GovernanceUser($id: ID!) {\n governanceUser(id: $id) {\n id\n allocatedLQTY\n stakedLQTY\n stakedOffset\n allocations {\n id\n atEpoch\n vetoLQTY\n voteLQTY\n initiative {\n id\n }\n }\n }\n }\n": types.GovernanceUserDocument, }; /** @@ -82,6 +83,10 @@ export function graphql(source: "\n query InterestRateBrackets($collId: String! * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query GovernanceInitiatives {\n governanceInitiatives {\n id\n }\n }\n"): typeof import('./graphql').GovernanceInitiativesDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query GovernanceUser($id: ID!) {\n governanceUser(id: $id) {\n id\n allocatedLQTY\n stakedLQTY\n stakedOffset\n allocations {\n id\n atEpoch\n vetoLQTY\n voteLQTY\n initiative {\n id\n }\n }\n }\n }\n"): typeof import('./graphql').GovernanceUserDocument; export function graphql(source: string) { diff --git a/frontend/app/src/graphql/graphql.ts b/frontend/app/src/graphql/graphql.ts index 35ce37962..fc8874e3f 100644 --- a/frontend/app/src/graphql/graphql.ts +++ b/frontend/app/src/graphql/graphql.ts @@ -499,6 +499,8 @@ export enum GovernanceAllocation_OrderBy { User = 'user', UserAllocatedLqty = 'user__allocatedLQTY', UserId = 'user__id', + UserStakedLqty = 'user__stakedLQTY', + UserStakedOffset = 'user__stakedOffset', VetoLqty = 'vetoLQTY', VoteLqty = 'voteLQTY' } @@ -614,6 +616,7 @@ export type GovernanceStats = { id: Scalars['ID']['output']; totalInitiatives: Scalars['Int']['output']; totalLQTYStaked: Scalars['BigInt']['output']; + totalOffset: Scalars['BigInt']['output']; }; export type GovernanceStats_Filter = { @@ -645,12 +648,21 @@ export type GovernanceStats_Filter = { totalLQTYStaked_lte?: InputMaybe; totalLQTYStaked_not?: InputMaybe; totalLQTYStaked_not_in?: InputMaybe>; + totalOffset?: InputMaybe; + totalOffset_gt?: InputMaybe; + totalOffset_gte?: InputMaybe; + totalOffset_in?: InputMaybe>; + totalOffset_lt?: InputMaybe; + totalOffset_lte?: InputMaybe; + totalOffset_not?: InputMaybe; + totalOffset_not_in?: InputMaybe>; }; export enum GovernanceStats_OrderBy { Id = 'id', TotalInitiatives = 'totalInitiatives', - TotalLqtyStaked = 'totalLQTYStaked' + TotalLqtyStaked = 'totalLQTYStaked', + TotalOffset = 'totalOffset' } export type GovernanceUser = { @@ -658,6 +670,8 @@ export type GovernanceUser = { allocatedLQTY: Scalars['BigInt']['output']; allocations: Array; id: Scalars['ID']['output']; + stakedLQTY: Scalars['BigInt']['output']; + stakedOffset: Scalars['BigInt']['output']; }; @@ -691,12 +705,30 @@ export type GovernanceUser_Filter = { id_not?: InputMaybe; id_not_in?: InputMaybe>; or?: InputMaybe>>; + stakedLQTY?: InputMaybe; + stakedLQTY_gt?: InputMaybe; + stakedLQTY_gte?: InputMaybe; + stakedLQTY_in?: InputMaybe>; + stakedLQTY_lt?: InputMaybe; + stakedLQTY_lte?: InputMaybe; + stakedLQTY_not?: InputMaybe; + stakedLQTY_not_in?: InputMaybe>; + stakedOffset?: InputMaybe; + stakedOffset_gt?: InputMaybe; + stakedOffset_gte?: InputMaybe; + stakedOffset_in?: InputMaybe>; + stakedOffset_lt?: InputMaybe; + stakedOffset_lte?: InputMaybe; + stakedOffset_not?: InputMaybe; + stakedOffset_not_in?: InputMaybe>; }; export enum GovernanceUser_OrderBy { AllocatedLqty = 'allocatedLQTY', Allocations = 'allocations', - Id = 'id' + Id = 'id', + StakedLqty = 'stakedLQTY', + StakedOffset = 'stakedOffset' } export type InterestBatch = { @@ -2244,6 +2276,13 @@ export type GovernanceInitiativesQueryVariables = Exact<{ [key: string]: never; export type GovernanceInitiativesQuery = { __typename?: 'Query', governanceInitiatives: Array<{ __typename?: 'GovernanceInitiative', id: string }> }; +export type GovernanceUserQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type GovernanceUserQuery = { __typename?: 'Query', governanceUser?: { __typename?: 'GovernanceUser', id: string, allocatedLQTY: bigint, stakedLQTY: bigint, stakedOffset: bigint, allocations: Array<{ __typename?: 'GovernanceAllocation', id: string, atEpoch: bigint, vetoLQTY: bigint, voteLQTY: bigint, initiative: { __typename?: 'GovernanceInitiative', id: string } }> } | null }; + export class TypedDocumentString extends String implements DocumentTypeDecoration @@ -2475,4 +2514,23 @@ export const GovernanceInitiativesDocument = new TypedDocumentString(` id } } - `) as unknown as TypedDocumentString; \ No newline at end of file + `) as unknown as TypedDocumentString; +export const GovernanceUserDocument = new TypedDocumentString(` + query GovernanceUser($id: ID!) { + governanceUser(id: $id) { + id + allocatedLQTY + stakedLQTY + stakedOffset + allocations { + id + atEpoch + vetoLQTY + voteLQTY + initiative { + id + } + } + } +} + `) as unknown as TypedDocumentString; \ No newline at end of file diff --git a/frontend/app/src/subgraph-hooks.ts b/frontend/app/src/subgraph-hooks.ts index 6ab28fd29..9bf10c02e 100644 --- a/frontend/app/src/subgraph-hooks.ts +++ b/frontend/app/src/subgraph-hooks.ts @@ -16,6 +16,7 @@ import { useQuery } from "@tanstack/react-query"; import * as dn from "dnum"; import { GovernanceInitiatives, + GovernanceUser, graphQuery, InterestBatchQuery, InterestRateBracketsQuery, @@ -420,6 +421,26 @@ export function useGovernanceInitiatives(options?: Options) { }); } +export function useGovernanceUser(account: Address | null, options?: Options) { + let queryFn = async () => { + if (!account) return null; + const { governanceUser } = await graphQuery(GovernanceUser, { + id: account.toLowerCase(), + }); + return governanceUser; + }; + + if (DEMO_MODE) { + queryFn = async () => null; + } + + return useQuery({ + queryKey: ["GovernanceUser", account], + queryFn, + ...prepareOptions(options), + }); +} + function subgraphTroveToLoan( trove: TrovesByAccountQueryType["troves"][number], ): PositionLoanCommitted { diff --git a/frontend/app/src/subgraph-queries.ts b/frontend/app/src/subgraph-queries.ts index d2eb58f7d..d4bdd9bb0 100644 --- a/frontend/app/src/subgraph-queries.ts +++ b/frontend/app/src/subgraph-queries.ts @@ -262,3 +262,23 @@ export const GovernanceInitiatives = graphql(` } } `); + +export const GovernanceUser = graphql(` + query GovernanceUser($id: ID!) { + governanceUser(id: $id) { + id + allocatedLQTY + stakedLQTY + stakedOffset + allocations { + id + atEpoch + vetoLQTY + voteLQTY + initiative { + id + } + } + } + } +`); diff --git a/frontend/uikit/src/react-utils.ts b/frontend/uikit/src/react-utils.ts index 41ab446b7..65ef6bfa7 100644 --- a/frontend/uikit/src/react-utils.ts +++ b/frontend/uikit/src/react-utils.ts @@ -35,15 +35,20 @@ export function useElementSize( return { size, ref }; } -export function useRaf(callback: (time: number) => void) { +export function useRaf(callback: (time: number) => void, fps = 60) { useEffect(() => { let rafId: number; let lastTime = 0; + let fpsInterval = 1000 / fps; const loop = (time: number) => { rafId = requestAnimationFrame(loop); - callback(time - lastTime); - lastTime = time; + const deltaTime = time - lastTime; + + if (deltaTime > fpsInterval) { + lastTime = time - (deltaTime % fpsInterval); + callback(time); + } }; rafId = requestAnimationFrame(loop); @@ -51,5 +56,5 @@ export function useRaf(callback: (time: number) => void) { return () => { cancelAnimationFrame(rafId); }; - }, [callback]); + }, [callback, fps]); }