Skip to content

Commit

Permalink
Merge pull request #672 from liquity/app-voting-power
Browse files Browse the repository at this point in the history
App: show live voting power
  • Loading branch information
danielattilasimon authored Dec 22, 2024
2 parents 194d3fa + c670662 commit 43c4278
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<HTMLDivElement>(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 (
<div
className={css({
Expand Down Expand Up @@ -251,6 +286,10 @@ export function StakePositionSummary({
style={style}
>
<div
ref={votingPowerRef}
className={css({
fontVariantNumeric: "tabular-nums",
})}
style={{
color: txPreviewMode
&& prevStakePosition
Expand All @@ -260,28 +299,23 @@ export function StakePositionSummary({
: "inherit",
}}
>
<Amount
percentage
value={stakePosition?.share ?? 0}
/>
</div>
{prevStakePosition && stakePosition && !dn.eq(prevStakePosition.share, stakePosition.share)
? (
<div
className={css({
color: "contentAlt",
textDecoration: "line-through",
})}
>
<Amount
percentage
value={prevStakePosition?.share ?? 0}
/>
</div>
)
: " of pool"}
{prevStakePosition && stakePosition && !dn.eq(prevStakePosition.share, stakePosition.share) && (
<div
className={css({
color: "contentAlt",
textDecoration: "line-through",
})}
>
<Amount
percentage
value={prevStakePosition?.share ?? 0}
/>
</div>
)}
<InfoTooltip>
Voting power is the percentage of the total staked LQTY that you own.
Voting power is the total staked LQTY that you own.<br /> It is calculated as:<br />
<code>lqty * t - offset</code>
</InfoTooltip>
</a.div>
)
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/src/graphql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

/**
Expand Down Expand Up @@ -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) {
Expand Down
64 changes: 61 additions & 3 deletions frontend/app/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -645,19 +648,30 @@ export type GovernanceStats_Filter = {
totalLQTYStaked_lte?: InputMaybe<Scalars['BigInt']['input']>;
totalLQTYStaked_not?: InputMaybe<Scalars['BigInt']['input']>;
totalLQTYStaked_not_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
totalOffset?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_gt?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_gte?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
totalOffset_lt?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_lte?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_not?: InputMaybe<Scalars['BigInt']['input']>;
totalOffset_not_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
};

export enum GovernanceStats_OrderBy {
Id = 'id',
TotalInitiatives = 'totalInitiatives',
TotalLqtyStaked = 'totalLQTYStaked'
TotalLqtyStaked = 'totalLQTYStaked',
TotalOffset = 'totalOffset'
}

export type GovernanceUser = {
__typename?: 'GovernanceUser';
allocatedLQTY: Scalars['BigInt']['output'];
allocations: Array<GovernanceAllocation>;
id: Scalars['ID']['output'];
stakedLQTY: Scalars['BigInt']['output'];
stakedOffset: Scalars['BigInt']['output'];
};


Expand Down Expand Up @@ -691,12 +705,30 @@ export type GovernanceUser_Filter = {
id_not?: InputMaybe<Scalars['ID']['input']>;
id_not_in?: InputMaybe<Array<Scalars['ID']['input']>>;
or?: InputMaybe<Array<InputMaybe<GovernanceUser_Filter>>>;
stakedLQTY?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_gt?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_gte?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
stakedLQTY_lt?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_lte?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_not?: InputMaybe<Scalars['BigInt']['input']>;
stakedLQTY_not_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
stakedOffset?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_gt?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_gte?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
stakedOffset_lt?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_lte?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_not?: InputMaybe<Scalars['BigInt']['input']>;
stakedOffset_not_in?: InputMaybe<Array<Scalars['BigInt']['input']>>;
};

export enum GovernanceUser_OrderBy {
AllocatedLqty = 'allocatedLQTY',
Allocations = 'allocations',
Id = 'id'
Id = 'id',
StakedLqty = 'stakedLQTY',
StakedOffset = 'stakedOffset'
}

export type InterestBatch = {
Expand Down Expand Up @@ -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<TResult, TVariables>
extends String
implements DocumentTypeDecoration<TResult, TVariables>
Expand Down Expand Up @@ -2475,4 +2514,23 @@ export const GovernanceInitiativesDocument = new TypedDocumentString(`
id
}
}
`) as unknown as TypedDocumentString<GovernanceInitiativesQuery, GovernanceInitiativesQueryVariables>;
`) as unknown as TypedDocumentString<GovernanceInitiativesQuery, GovernanceInitiativesQueryVariables>;
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<GovernanceUserQuery, GovernanceUserQueryVariables>;
21 changes: 21 additions & 0 deletions frontend/app/src/subgraph-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useQuery } from "@tanstack/react-query";
import * as dn from "dnum";
import {
GovernanceInitiatives,
GovernanceUser,
graphQuery,
InterestBatchQuery,
InterestRateBracketsQuery,
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions frontend/app/src/subgraph-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
`);
13 changes: 9 additions & 4 deletions frontend/uikit/src/react-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,26 @@ export function useElementSize<T extends HTMLElement>(
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);

return () => {
cancelAnimationFrame(rafId);
};
}, [callback]);
}, [callback, fps]);
}

0 comments on commit 43c4278

Please sign in to comment.