Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use bigint for allocation computations to avoid number conversion error #499

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions apps/web/app/(app)/gardens/[chain]/[garden]/[community]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
RectangleGroupIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { Dnum } from "dnum";
import { Dnum, multiply } from "dnum";
import Image from "next/image";
import Link from "next/link";
import { Address } from "viem";
Expand Down Expand Up @@ -41,7 +41,6 @@ import { useSubgraphQuery } from "@/hooks/useSubgraphQuery";
import { PoolTypes } from "@/types";
import { fetchIpfs } from "@/utils/ipfsUtils";
import {
dn,
parseToken,
SCALE_PRECISION,
SCALE_PRECISION_DECIMALS,
Expand Down Expand Up @@ -214,17 +213,17 @@ export default function Page({
const membership = [
BigInt(registerStakeAmount),
Number(tokenGarden!.decimals),
] as dn.Dnum;
] as Dnum;
const feePercentage = [
BigInt(communityFee),
SCALE_PRECISION_DECIMALS, // adding 2 decimals because 1% == 10.000 == 1e4
] as dn.Dnum;
] as Dnum;

return dn.multiply(membership, feePercentage);
return multiply(membership, feePercentage);
} catch (err) {
console.error(err);
}
return [0n, 0] as dn.Dnum;
return [0n, 0] as Dnum;
};

const registrationAmount = [
Expand Down
6 changes: 3 additions & 3 deletions apps/web/components/PoolGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import {
} from "@/components/";

export type PoolGovernanceProps = {
memberPoolWeight: number;
memberPoolWeight: number | undefined;
tokenDecimals: number;
strategy: Pick<CVStrategy, "id" | "sybilScorer" | "poolId"> & {
registryCommunity: { garden: Pick<TokenGarden, "symbol"> };
config: Pick<CVStrategyConfig, "pointSystem" | "allowlist">;
};
communityAddress: Address;
memberTokensInCommunity: number;
memberTokensInCommunity: bigint;
isMemberCommunity: boolean;
memberActivatedStrategy: boolean;
};
Expand Down Expand Up @@ -89,7 +89,7 @@ export const PoolGovernance: React.FC<PoolGovernanceProps> = ({
<div className="flex items-start gap-6">
<p className="subtitle2">Your voting weight:</p>
<p className="subtitle2 text-primary-content">
{memberPoolWeight.toFixed(2)} %
{memberPoolWeight?.toFixed(2)} %
</p>
</div>
)}
Expand Down
35 changes: 21 additions & 14 deletions apps/web/components/ProposalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
} from "@/hooks/useConvictionRead";
import { useMetadataIpfsFetch } from "@/hooks/useIpfsFetch";
import { PoolTypes, ProposalStatus } from "@/types";
import { calculatePercentage } from "@/utils/numbers";
import {
calculatePercentage,
calculatePercentageBigInt,
} from "@/utils/numbers";
import { prettyTimestamp } from "@/utils/text";

export type ProposalCardProps = {
Expand All @@ -45,13 +48,13 @@ export type ProposalCardProps = {
stakedFilter: ProposalInputItem;
poolToken?: FetchTokenResult;
isAllocationView: boolean;
memberActivatedPoints: number;
memberActivatedPoints: bigint;
memberPoolWeight: number;
executeDisabled: boolean;
tokenDecimals: number;
alloInfo: Allo;
tokenData: Parameters<typeof useConvictionRead>[0]["tokenData"];
inputHandler: (proposalId: string, value: number) => void;
inputHandler: (proposalId: string, value: bigint) => void;
};

export function ProposalCard({
Expand Down Expand Up @@ -92,12 +95,13 @@ export function ProposalCard({
strategyConfig,
tokenData,
});

const inputValue =
inputData ? calculatePercentage(inputData.value, memberActivatedPoints) : 0;
inputData ?
calculatePercentageBigInt(inputData.value, memberActivatedPoints)
: 0;

const poolWeightAllocatedInProposal = (
(inputValue * memberPoolWeight) /
(inputValue * Number(memberPoolWeight)) /
100
).toFixed(2);

Expand Down Expand Up @@ -215,13 +219,16 @@ export function ProposalCard({
<input
type="range"
min={0}
max={memberActivatedPoints}
value={inputData?.value}
max={Number(memberActivatedPoints)}
value={inputData ? Number(inputData.value) : undefined}
className={`range range-md cursor-pointer bg-neutral-soft [--range-shdw:var(--color-green-500)] ${isProposalEnded ? "grayscale !cursor-not-allowed" : ""}`}
step={memberActivatedPoints / 100}
onChange={(e) =>
inputHandler(proposalData.id, Number(e.target.value))
}
step={Number(memberActivatedPoints / 100n)}
onChange={(e) => {
inputHandler(
proposalData.id,
BigInt(Math.floor(Number(e.target.value))),
);
}}
disabled={isProposalEnded}
/>
</div>
Expand All @@ -235,11 +242,11 @@ export function ProposalCard({
</div>
</div>

{isProposalEnded && inputData?.value != 0 && (
{isProposalEnded && inputData?.value != 0n && (
<Button
className="mb-2 !p-2 !px-3"
btnStyle="outline"
onClick={() => inputHandler(proposalData.id, 0)}
onClick={() => inputHandler(proposalData.id, 0n)}
tooltip="Clear allocation"
>
&times;
Expand Down
98 changes: 53 additions & 45 deletions apps/web/components/Proposals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ import { useSubgraphQuery } from "@/hooks/useSubgraphQuery";
import { alloABI, registryCommunityABI } from "@/src/generated";
import { ProposalStatus } from "@/types";
import { useErrorDetails } from "@/utils/getErrorName";
import { calculatePercentage } from "@/utils/numbers";
import { bigIntMin, calculatePercentageBigInt } from "@/utils/numbers";

// Types
export type ProposalInputItem = {
proposalId: string;
proposalNumber: number;
value: number;
value: bigint;
};

// export type Strategy = getStrategyByPoolQuery["cvstrategies"][number];
Expand All @@ -63,7 +63,7 @@ export type ProposalTypeVoter = CVProposal & {
type Stats = {
id: number;
name: string;
stat: number | string;
stat: number | undefined;
className: string;
info: string;
};
Expand Down Expand Up @@ -98,11 +98,10 @@ export function Proposals({
}: ProposalsProps) {
// State
const [allocationView, setAllocationView] = useState(false);
const [inputAllocatedTokens, setInputAllocatedTokens] = useState<number>(0);
const [inputAllocatedTokens, setInputAllocatedTokens] = useState<bigint>(0n);
const [inputs, setInputs] = useState<{ [key: string]: ProposalInputItem }>(
{},
);
const [memberActivatedPoints, setMemberActivatedPoints] = useState<number>(0);
const [stakedFilters, setStakedFilters] = useState<{
[key: string]: ProposalInputItem;
}>({});
Expand All @@ -117,6 +116,7 @@ export function Proposals({

const tokenDecimals = strategy.registryCommunity.garden.decimals;
const searchParams = useCollectQueryParams();

// Queries
const { data: memberData, error } = useSubgraphQuery<isMemberQuery>({
query: isMemberDocument,
Expand Down Expand Up @@ -157,6 +157,10 @@ export function Proposals({
},
);

const memberActivatedPoints: bigint = BigInt(
memberStrategyData?.memberStrategy?.activatedPoints ?? 0,
);

// Contract reads
const { data: memberPower, refetch: refetchMemberPower } = useContractRead({
address: communityAddress,
Expand Down Expand Up @@ -191,9 +195,10 @@ export function Proposals({
const isMemberCommunity =
!!memberData?.member?.memberCommunity?.[0]?.isRegistered;
const memberActivatedStrategy =
Number(memberStrategyData?.memberStrategy?.activatedPoints) > 0;
const memberTokensInCommunity =
memberData?.member?.memberCommunity?.[0]?.stakedTokens ?? 0;
memberStrategyData?.memberStrategy?.activatedPoints > 0n;
const memberTokensInCommunity = BigInt(
memberData?.member?.memberCommunity?.[0]?.stakedTokens ?? 0,
);

const proposals = strategy.proposals;

Expand All @@ -206,36 +211,32 @@ export function Proposals({

useEffect(() => {
const stakesFiltered =
memberData?.member?.stakes?.filter(
(stake) =>
stake.proposal.strategy.id.toLowerCase() ===
strategy.id.toLowerCase(),
) ?? [];
memberData?.member?.stakes
?.filter(
(stake) =>
stake.proposal.strategy.id.toLowerCase() ===
strategy.id.toLowerCase(),
)
.map((x) => ({ ...x, amount: BigInt(x.amount) })) ?? [];

const totalStaked = stakesFiltered.reduce(
(acc, curr) => acc + BigInt(curr.amount),
(acc, curr) => acc + curr.amount,
0n,
);

const memberStakes: { [key: string]: ProposalInputItem } = {};
stakesFiltered.forEach((item) => {
memberStakes[item.proposal.id] = {
proposalId: item.proposal.id,
value: Number(item.amount),
value: item.amount,
proposalNumber: item.proposal.proposalNumber,
};
});

setInputAllocatedTokens(Number(totalStaked));
setInputAllocatedTokens(totalStaked);
setStakedFilters(memberStakes);
}, [memberData?.member?.stakes, strategy.id]);

useEffect(() => {
setMemberActivatedPoints(
Number(memberStrategyData?.memberStrategy?.activatedPoints ?? 0n),
);
}, [memberStrategyData?.memberStrategy?.activatedPoints]);

useEffect(() => {
if (memberActivatedStrategy === false) {
setAllocationView(false);
Expand Down Expand Up @@ -276,7 +277,7 @@ export function Proposals({
proposals.forEach(({ id, proposalNumber }) => {
newInputs[id] = {
proposalId: id,
value: stakedFilters[id]?.value ?? 0,
value: stakedFilters[id]?.value ?? 0n,
proposalNumber,
};
});
Expand All @@ -287,12 +288,12 @@ export function Proposals({
inputData: { [key: string]: ProposalInputItem },
currentData: { [key: string]: ProposalInputItem },
) => {
// log maximum stakable tokens
return Object.values(inputData).reduce<
{ proposalId: bigint; deltaSupport: bigint }[]
>((acc, input) => {
const current = currentData[input.proposalId];
const diff =
BigInt(Math.floor(input.value)) - BigInt(current?.value ?? 0);
const diff = BigInt(input.value) - BigInt(current?.value ?? 0);
if (diff !== 0n) {
acc.push({
proposalId: BigInt(input.proposalNumber),
Expand All @@ -306,7 +307,7 @@ export function Proposals({
const calculateTotalTokens = (exceptProposalId?: string) => {
if (!inputs) {
console.error("Inputs not yet computed");
return 0;
return 0n;
}
return Object.values(inputs).reduce((acc, curr) => {
if (
Expand All @@ -315,20 +316,21 @@ export function Proposals({
) {
return acc;
} else {
return acc + Number(curr.value);
return acc + curr.value;
}
}, 0);
}, 0n);
};

const inputHandler = (proposalId: string, value: number) => {
const inputHandler = (proposalId: string, value: bigint) => {
const currentPoints = calculateTotalTokens(proposalId);

const maxAllowableValue = memberActivatedPoints - currentPoints;
value = Math.min(value, maxAllowableValue);
const minValue = (value = bigIntMin(value, maxAllowableValue));

const input = inputs[proposalId];
input.value = value;
input.value = minValue;
setInputs((prev) => ({ ...prev, [proposalId]: input }));
setInputAllocatedTokens(currentPoints + value);
setInputAllocatedTokens(currentPoints + minValue);
};

const toastId = useRef<Id | null>(null);
Expand Down Expand Up @@ -371,7 +373,6 @@ export function Proposals({
inputs,
stakedFilters,
);
console.debug("Proposal Deltas", proposalsDifferencesArr);
const abiTypes = parseAbiParameters(
"(uint256 proposalId, int256 deltaSupport)[]",
);
Expand All @@ -387,22 +388,29 @@ export function Proposals({
useErrorDetails(errorAllocate, "errorAllocate");

// Computed values
const memberSupportedProposalsPct = calculatePercentage(
const memberSupportedProposalsPct = calculatePercentageBigInt(
inputAllocatedTokens,
memberActivatedPoints,
);
const memberPoolWeight = calculatePercentage(
Number(memberPower),
strategy.totalEffectiveActivePoints,
);

const calcPoolWeightUsed = (number: number) => {
if (memberPoolWeight == 0) return 0;
return ((number / 100) * memberPoolWeight).toFixed(2);
};
const memberPoolWeight =
memberPower != null && +strategy.totalEffectiveActivePoints > 0 ?
calculatePercentageBigInt(
memberPower,
BigInt(strategy.totalEffectiveActivePoints),
)
: undefined;

const calcPoolWeightUsed =
memberPoolWeight ?
(number: number) => {
if (memberPoolWeight == 0) return 0;
return ((number / 100) * memberPoolWeight).toFixed(2);
}
: undefined;

const poolWeightClassName = `${
calcPoolWeightUsed(memberSupportedProposalsPct) === memberPoolWeight ?
calcPoolWeightUsed?.(memberSupportedProposalsPct) === memberPoolWeight ?
"bg-secondary-soft text-secondary-content"
: "bg-primary-soft text-primary-content"
}`;
Expand Down Expand Up @@ -475,7 +483,7 @@ export function Proposals({
{allocationView && <UserAllocationStats stats={stats} />}
</div>
<div className="flex flex-col gap-6">
{proposals && inputs ?
{proposals && inputs && memberPoolWeight != null ?
<>
{proposals
.filter(
Expand All @@ -492,7 +500,7 @@ export function Proposals({
stakedFilter={stakedFilters[proposalData.id]}
isAllocationView={allocationView}
memberActivatedPoints={memberActivatedPoints}
memberPoolWeight={memberPoolWeight}
memberPoolWeight={memberPoolWeight ?? 0}
executeDisabled={
proposalData.proposalStatus == 4 ||
!isConnected ||
Expand Down
Loading
Loading