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

Show court payouts #2387 #2403

Merged
merged 21 commits into from
May 6, 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
6 changes: 3 additions & 3 deletions components/court/CourtExitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ const CourtExitButton = ({ className }: { className?: string }) => {
<>
<button
className={`rounded-md ${
canExit ? "bg-[#DC056C]" : "bg-gray-400"
canExit ? "bg-[#670031]" : "bg-gray-400"
} px-4 py-2 text-white ${className}`}
onClick={() => setIsOpen(true)}
disabled={!canExit}
>
<div className="flex items-center gap-1">
{canExit ? "Exit" : "Preparing to exit"}
<div className="flex items-center justify-center gap-1">
<span>{canExit ? "Exit" : "Preparing to exit"}</span>
{!canExit && (
<span className="text-xs text-gray-500">
({moment.duration(cooldownTime?.left).humanize()} left)
Expand Down
2 changes: 1 addition & 1 deletion components/court/CourtUnstakeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const CourtUnstakeButton = ({ className }: { className?: string }) => {
return (
<>
<button
className={`rounded-md bg-[#DC056C] px-4 py-2 text-white ${className}`}
className={`rounded-md bg-[#670031] px-4 py-2 text-white ${className}`}
onClick={() => setIsOpen(true)}
>
Unstake
Expand Down
2 changes: 1 addition & 1 deletion components/court/JoinCourtAsJurorButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const JoinCourtAsJurorButton = ({ className }: { className?: string }) => {
<div className="relative">
<button
disabled={isLoading}
className={`rounded-md bg-[#670031] px-4 py-2 text-white transition-all ${
className={`rounded-md bg-[#DC056C] px-4 py-2 text-white transition-all ${
connectedParticipant?.type === "Delegator" &&
"ring-2 ring-orange-500"
} ${className}`}
Expand Down
2 changes: 1 addition & 1 deletion components/court/ManageDelegationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ManageDelegationButton = ({ className }: { className?: string }) => {
<>
<div className="relative">
<button
className={`rounded-md bg-[#670031] px-4 py-2 text-white transition-all ${
className={`rounded-md bg-[#DC056C] px-4 py-2 text-white transition-all ${
connectedParticipant?.type === "Juror" && "ring-2 ring-orange-500"
} ${className}`}
onClick={() => setIsOpen(true)}
Expand Down
140 changes: 140 additions & 0 deletions components/portfolio/CourtRewardsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import SubScanIcon from "components/icons/SubScanIcon";
import Table, { TableColumn, TableData } from "components/ui/Table";
import Decimal from "decimal.js";
import { ZTG } from "lib/constants";
import { useChainConstants } from "lib/hooks/queries/useChainConstants";
import { useMintedInCourt } from "lib/hooks/queries/useMintedInCourt";
import { useZtgPrice } from "lib/hooks/queries/useZtgPrice";
import { formatNumberLocalized } from "lib/util";
import EmptyPortfolio from "./EmptyPortfolio";
import {
isPayoutEligible,
useCourtNextPayout,
} from "lib/hooks/queries/useCourtNextPayout";
import { times } from "lodash-es";
import { isNotNull } from "@zeitgeistpm/utility/dist/null";
import InfoPopover from "components/ui/InfoPopover";
import { PiTimerBold } from "react-icons/pi";

const columns: TableColumn[] = [
{
header: "Time",
accessor: "timestamp",
type: "component",
},
{
header: "Amount",
accessor: "amount",
type: "component",
alignment: "right",
},
{
header: "Subscan",
accessor: "subscan",
type: "component",
width: "100px",
},
];

const CourtRewardsTable = ({ address }: { address: string }) => {
const { data: mintedInCourt, isLoading } = useMintedInCourt({
account: address,
});
const { data: ztgPrice } = useZtgPrice();
const { data: constants } = useChainConstants();

const { data: courtPayout } = useCourtNextPayout();

let tableData: TableData[] | undefined = mintedInCourt?.map((mint) => {
return {
timestamp: (
<span>
{new Intl.DateTimeFormat("default", {
dateStyle: "medium",
timeStyle: "medium",
}).format(new Date(mint?.timestamp))}
</span>
),
amount: (
<div>
<div>
{formatNumberLocalized(
new Decimal(mint?.dBalance ?? 0).div(ZTG).toNumber(),
)}{" "}
<b>{constants?.tokenSymbol}</b>
</div>
<div className="text-gray-400">
${" "}
{formatNumberLocalized(
ztgPrice
?.mul(mint?.dBalance ?? 0)
.div(ZTG)
.toNumber() ?? 0,
)}
</div>
</div>
),
subscan: (
<a
className="center text-sm"
target="_blank"
referrerPolicy="no-referrer"
rel="noopener"
href={`https://zeitgeist.subscan.io/block/${mint?.blockNumber}?tab=event`}
>
<div className="">
<SubScanIcon />
</div>
</a>
),
};
});

tableData = [
isPayoutEligible(courtPayout)
? {
timestamp: (
<span className="flex items-center gap-2 text-gray-400">
{new Intl.DateTimeFormat("default", {
dateStyle: "medium",
timeStyle: "medium",
}).format(courtPayout.nextRewardDate)}
<span className="flex items-center gap-1 italic">
(ETA <PiTimerBold size={18} />)
</span>
</span>
),
amount: <div className="text-gray-300">--</div>,
subscan: (
<div className="center text-center">
<InfoPopover
icon={<PiTimerBold className="text-orange-300" size={24} />}
position={"top-start"}
>
Next expected staking reward payout.
</InfoPopover>
</div>
),
}
: null,
...(tableData ?? []),
].filter(isNotNull);

return (
<div>
{isLoading === false &&
(mintedInCourt == null || mintedInCourt?.length === 0) ? (
<EmptyPortfolio
headerText="No Court Rewards"
bodyText=""
buttonText="Go To Court"
buttonLink="/court"
/>
) : (
<Table columns={columns} data={tableData} />
)}
</div>
);
};

export default CourtRewardsTable;
32 changes: 32 additions & 0 deletions components/portfolio/CourtTabGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Tab } from "@headlessui/react";
import SubTabsList from "components/ui/SubTabsList";
import { useQueryParamState } from "lib/hooks/useQueryParamState";
import CourtRewardsTable from "./CourtRewardsTable";

type CourtGroupItem = "Rewards";
const courtTabItems: CourtGroupItem[] = ["Rewards"];

const CourtTabGroup = ({ address }: { address: string }) => {
const [historyTabSelection, setHistoryTabSelection] =
useQueryParamState<CourtGroupItem>("courtTab");

const courtTabIndex = courtTabItems.indexOf(historyTabSelection);
const selectedIndex = courtTabIndex !== -1 ? courtTabIndex : 0;

return (
<Tab.Group
defaultIndex={0}
selectedIndex={selectedIndex}
onChange={(index) => setHistoryTabSelection(courtTabItems[index])}
>
<SubTabsList titles={courtTabItems} />
<Tab.Panels>
<Tab.Panel>
<CourtRewardsTable address={address} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
};

export default CourtTabGroup;
2 changes: 1 addition & 1 deletion components/ui/InfoPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const InfoPopover: React.FC<InfoPopoverProps> = ({
<Popover.Panel
className={`absolute z-[100] bg-tooltip-bg ${positionCss} w-screen rounded-md lg:w-[564px] ${popoverCss}`}
>
<div className="shadow-xs overflow-hidden rounded-md p-5 text-left text-base font-light text-black ring-2 ring-orange-400 ring-opacity-20">
<div className="shadow-xs overflow-hidden rounded-md px-3 py-2 text-left text-sm font-light text-black ring-2 ring-orange-400 ring-opacity-20">
{children}
</div>
</Popover.Panel>
Expand Down
146 changes: 146 additions & 0 deletions lib/hooks/queries/useCourtNextPayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useQuery } from "@tanstack/react-query";
import { HistoricalAccountBalanceOrderByInput } from "@zeitgeistpm/indexer";
import { IndexerContext, isIndexedSdk, Sdk } from "@zeitgeistpm/sdk";
import { blockDate } from "@zeitgeistpm/utility/dist/time";
import Decimal from "decimal.js";
import { useChainTime } from "lib/state/chaintime";
import { useWallet } from "lib/state/wallet";
import { useSdkv2 } from "../useSdkv2";
import { useChainConstants } from "./useChainConstants";

export const courtNextPayoutRootKey = "court-next-payout";

export type CourtPayoutInfo = {
inflationPeriod: number;
nextPayoutBlock: number;
lastPayoutBlock: number;
nextPayoutDate: Date;
lastPayoutDate: Date;
};

export type WithPayoutEligibility = CourtPayoutInfo & {
nextRewardBlock: number;
nextRewardDate: Date;
};

export const isPayoutEligible = (
info?: CourtPayoutInfo | WithPayoutEligibility | null,
): info is WithPayoutEligibility =>
(info as WithPayoutEligibility)?.nextRewardBlock !== undefined;

export const useCourtNextPayout = () => {
const [sdk, id] = useSdkv2();
const now = useChainTime();
const { data: constants } = useChainConstants();
const wallet = useWallet();

const enabled = isIndexedSdk(sdk) && now && constants && wallet.realAddress;

const query = useQuery<CourtPayoutInfo | WithPayoutEligibility | null>(
[id, courtNextPayoutRootKey, wallet?.realAddress, now?.block],
async () => {
if (enabled) {
/**
* @note
* last_payout_block(current_block) = floor(current_block / inf_per) * inf_per
* next_payout_block(current_block) = last_payout_block(current_block) + inf_per
*/

const currentBlock = new Decimal(now.block);

const inflationPeriod = new Decimal(
constants.court.inflationPeriodBlocks,
);

const lastPayoutBlock = new Decimal(currentBlock)
.div(inflationPeriod)
.floor()
.mul(inflationPeriod);

const nextPayoutBlock = lastPayoutBlock.add(inflationPeriod);

const participantFirstJoinedAt = await getAccountJoined(
sdk,
wallet.realAddress!,
);

const courtPayoutInfo: CourtPayoutInfo = {
inflationPeriod: inflationPeriod.toNumber(),
nextPayoutBlock: nextPayoutBlock.toNumber(),
lastPayoutBlock: lastPayoutBlock.toNumber(),
nextPayoutDate: blockDate(now, nextPayoutBlock.toNumber()),
lastPayoutDate: blockDate(now, lastPayoutBlock.toNumber()),
};

if (participantFirstJoinedAt) {
const withPayoutEligibility: WithPayoutEligibility = {
...courtPayoutInfo,
nextRewardBlock: nextPayoutBlock.toNumber(),
nextRewardDate: blockDate(now, nextPayoutBlock.toNumber()),
};

return withPayoutEligibility;
}

return courtPayoutInfo;
}

return null;
},
{
enabled: Boolean(enabled),
keepPreviousData: true,
},
);

return query;
};

const getAccountJoined = async (sdk: Sdk<IndexerContext>, address: string) => {
const { historicalAccountBalances } =
await sdk.indexer.historicalAccountBalances({
where: {
AND: [
{ accountId_eq: address },
{
OR: [
{
extrinsic: {
name_eq: "Court.join_court",
},
},
{
extrinsic: {
name_eq: "Court.delegate",
},
},

{
extrinsic: {
name_eq: "Court.exit_court",
},
},
],
},
],
},
order: HistoricalAccountBalanceOrderByInput.BlockNumberDesc,
});

let earliestEligibleJoin: Decimal | null = null;

for (const event of historicalAccountBalances) {
if (
event.extrinsic?.name === "Court.join_court" ||
event.extrinsic?.name === "Court.delegate"
) {
earliestEligibleJoin = new Decimal(event.blockNumber);
}
if (event.extrinsic?.name === "Court.exit_court") {
console.log("EXITED");
break;
}
}

return earliestEligibleJoin;
};
Loading
Loading