Skip to content

Commit

Permalink
Merge pull request #931 from tablelandnetwork/datadanne/add-ft-leader…
Browse files Browse the repository at this point in the history
…board

Garage: Add FT leaderboard
  • Loading branch information
sanderpick authored Oct 24, 2023
2 parents c7dce40 + 67c46ea commit 71af0ad
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 2 deletions.
30 changes: 30 additions & 0 deletions garage/src/hooks/useRigStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
selectAccountStats,
selectTopActivePilotCollections,
selectTopFtPilotCollections,
selectTopFtEarners,
} from "~/utils/queries";
import { useTablelandConnection } from "./useTablelandConnection";

Expand Down Expand Up @@ -160,3 +161,32 @@ export const useTopFtPilotCollections = () => {

return { stats };
};

interface FtLeaderboardEntry {
address: string;
ft: number;
}

export const useFtLeaderboard = (first: number) => {
const { db } = useTablelandConnection();

const [stats, setStats] = useState<FtLeaderboardEntry[]>();

useEffect(() => {
let isCancelled = false;

db.prepare(selectTopFtEarners(first))
.all<FtLeaderboardEntry>()
.then(({ results }) => {
if (isCancelled) return;

setStats(results);
});

return () => {
isCancelled = true;
};
}, [setStats]);

return { stats };
};
96 changes: 94 additions & 2 deletions garage/src/pages/Dashboard/modules/Stats.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import {
Box,
Heading,
Expand All @@ -21,17 +22,22 @@ import {
TabPanel,
TabPanels,
Text,
useBreakpointValue,
Show,
} from "@chakra-ui/react";
import { usePublicClient } from "wagmi";
import { useAccount } from "~/hooks/useAccount";
import {
useAccountStats,
useStats,
useTopActivePilotCollections,
useTopFtPilotCollections,
useFtLeaderboard,
Stat,
} from "~/hooks/useRigStats";
import { useNFTCollections, Collection } from "~/hooks/useNFTs";
import { prettyNumber } from "~/utils/fmt";
import { prettyNumber, truncateWalletAddress } from "~/utils/fmt";
import { isValidAddress } from "~/utils/types";

const StatItem = ({ name, value }: { name: string; value: number }) => {
return (
Expand Down Expand Up @@ -107,12 +113,88 @@ const CollectionToplist = ({
);
};

const FTLeaderboard = ({
data,
}: {
data: { address: string; ft: number }[];
}) => {
const publicClient = usePublicClient();

const [ensNames, setEnsNames] = useState<Record<string, string>>({});

// Effect that reverse-resolves address->ens for all FT leaderboard entries
// in one batch multicall request
useEffect(() => {
let isCancelled = false;
if (data.length > 0) {
Promise.all(
data
.map(({ address }) => address)
.filter(isValidAddress)
.map(async (address) => {
const ens = await publicClient.getEnsName({ address });
return [address, ens];
})
).then((v) => {
if (!isCancelled)
setEnsNames(Object.fromEntries(v.filter(([, ens]) => ens)));
});
}
return () => {
isCancelled = true;
};
}, [data]);

const { actingAsAddress } = useAccount();

const shouldTruncate = useBreakpointValue({
base: true,
md: false,
});

return (
<Table>
<Thead>
<Tr>
<Th>Address</Th>
<Th isNumeric>FT</Th>
</Tr>
</Thead>
<Tbody>
{data?.slice(0, 15).map(({ address, ft }, index) => {
const isUser =
!!actingAsAddress &&
actingAsAddress.toLowerCase() === address?.toLowerCase();

const truncatedAddress = address
? truncateWalletAddress(address)
: "";
const name = isUser
? "You"
: ensNames[address] ??
(shouldTruncate ? truncatedAddress : address);

return (
<Tr key={`pilot-${index}`}>
<Td>
<RouterLink to={`/owner/${address}`}>{name}</RouterLink>
</Td>
<Td isNumeric>{prettyNumber(ft)}</Td>
</Tr>
);
})}
</Tbody>
</Table>
);
};

export const Stats = (props: React.ComponentProps<typeof Box>) => {
const { actingAsAddress } = useAccount();
const { stats } = useStats();
const { stats: accountStats } = useAccountStats(actingAsAddress);
const { stats: pilotStats } = useTopActivePilotCollections();
const { stats: ftStats } = useTopFtPilotCollections();
const { stats: ftLeaderboard } = useFtLeaderboard(15);

const contracts = useMemo(() => {
if (!pilotStats || !ftStats) return;
Expand All @@ -139,6 +221,10 @@ export const Stats = (props: React.ComponentProps<typeof Box>) => {
<Tab>Global</Tab>
{actingAsAddress && <Tab>You</Tab>}
<Tab>Pilots</Tab>
<Tab>
<Show above="sm">FT Leaderboard</Show>
<Show below="sm">FT</Show>
</Tab>
</TabList>

<TabPanels>
Expand Down Expand Up @@ -187,6 +273,12 @@ export const Stats = (props: React.ComponentProps<typeof Box>) => {
</VStack>
</Flex>
</TabPanel>
<TabPanel fontSize="0.8em" px={0}>
<VStack flexGrow="1" pt={2} align="start">
<Heading>Top FT earners</Heading>
{ftLeaderboard && <FTLeaderboard data={ftLeaderboard} />}
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</Flex>
Expand Down
25 changes: 25 additions & 0 deletions garage/src/utils/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,31 @@ export const selectTopFtPilotCollections = (): string => {
ORDER BY ft DESC`;
};

export const selectTopFtEarners = (
first: number,
offset: number = 0
): string => {
return `
SELECT
address,
sum(ft) AS "ft"
FROM (
SELECT
owner AS "address",
(coalesce(end_time, BLOCK_NUM(${chain.id})) - start_time) as "ft"
FROM ${pilotSessionsTable}
UNION ALL
SELECT
recipient AS "address",
amount AS "ft"
FROM ${ftRewardsTable}
)
GROUP BY address
ORDER BY ft DESC
LIMIT ${first}
OFFSET ${offset}`;
};

export const selectPilotSessionsForPilot = (
contract: string,
tokenId: string
Expand Down

0 comments on commit 71af0ad

Please sign in to comment.