Skip to content

Commit

Permalink
feat: add sorting to delegation and worker pages
Browse files Browse the repository at this point in the history
  • Loading branch information
belopash committed Jun 14, 2024
1 parent 262f34a commit 3a6c7ef
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 97 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"prettier": "^3.2.5",
"type-fest": "^4.20.0",
"typescript": "^5.4.3",
"vite": "^5.2.6",
"vite-tsconfig-paths": "^4.3.2"
Expand Down
167 changes: 110 additions & 57 deletions src/api/subsquid-network-squid/workers-graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { calculateDelegationCapacity } from '@lib/network';
import BigNumber from 'bignumber.js';
import { groupBy, mapValues, values } from 'lodash-es';
import { PartialDeep, SimplifyDeep } from 'type-fest';

import { useAccount } from '@network/useAccount.ts';

Expand All @@ -20,10 +21,8 @@ import {
useWorkerDelegationInfoQuery,
useWorkerOwnerQuery,
Worker,
WorkerFragmentFragment,
} from './graphql';
import { useNetworkSettings } from './settings-graphql';

// inherit API interface for internal class
// export interface BlockchainApiWorker extends Omit<WorkerFragmentFragment, 'createdAt'> {
// createdAt: Date;
Expand Down Expand Up @@ -121,6 +120,9 @@ import { useNetworkSettings } from './settings-graphql';

export interface WorkerExtended extends Worker {
delegationCapacity: number;
myDelegation: string;
myTotalDelegationReward: string;
totalReward: string;
}

export enum WorkerSortBy {
Expand All @@ -129,14 +131,52 @@ export enum WorkerSortBy {
Uptime90d = 'uptime_90d',
StakerAPR = 'staker_apr',
WorkerAPR = 'apr',
WorkerReward = 'worker_reward',
DelegationCapacity = 'delegation_capacity',
MyDelegation = 'my_delegation',
MyDelegationReward = 'my_delegation_reward',
}

export enum SortDir {
Asc = 'asc',
Desc = 'desc',
}

export function sortWorkers<T extends PartialDeep<WorkerExtended, { recurseIntoArrays: true }>>(
workers: T[],
sortBy: WorkerSortBy,
sortDir: SortDir,
) {
return workers.sort((a, b) => {
if (sortDir === SortDir.Desc) {
[a, b] = [b, a];
}

switch (sortBy) {
case WorkerSortBy.Uptime90d:
return (a.uptime90Days ?? -1) - (b.uptime90Days ?? -1);
case WorkerSortBy.Uptime24h:
return (a.uptime24Hours ?? -1) - (b.uptime24Hours ?? -1);
case WorkerSortBy.DelegationCapacity:
return (a.delegationCapacity ?? -1) - (b.delegationCapacity ?? -1);
case WorkerSortBy.StakerAPR:
return (a.stakerApr ?? -1) - (b.stakerApr ?? -1);
case WorkerSortBy.WorkerAPR:
return (a.apr ?? -1) - (b.apr ?? -1);
case WorkerSortBy.WorkerReward:
return BigInt(a.totalReward ?? -1) > BigInt(b.totalReward ?? -1) ? 1 : -1;
case WorkerSortBy.MyDelegation:
return BigInt(a.myDelegation ?? -1) > BigInt(b.myDelegation ?? -1) ? 1 : -1;
case WorkerSortBy.MyDelegationReward:
return BigInt(a.myTotalDelegationReward ?? -1) > BigInt(b.myTotalDelegationReward ?? -1)
? 1
: -1;
default:
return new Date(a.createdAt || 0).valueOf() - new Date(b.createdAt || 0).valueOf();
}
});
}

export function useWorkers({
page,
perPage,
Expand Down Expand Up @@ -174,24 +214,6 @@ export function useWorkers({
capedDelegation: w.capedDelegation,
}),
};
})
.sort((a, b) => {
if (sortDir === SortDir.Desc) {
[a, b] = [b, a];
}

switch (sortBy) {
case WorkerSortBy.Uptime90d:
return (a.uptime90Days ?? -1) - (b.uptime90Days ?? -1);
case WorkerSortBy.DelegationCapacity:
return (a.delegationCapacity ?? -1) - (b.delegationCapacity ?? -1);
case WorkerSortBy.StakerAPR:
return (a.stakerApr ?? -1) - (b.stakerApr ?? -1);
case WorkerSortBy.WorkerAPR:
return (a.apr ?? -1) - (b.apr ?? -1);
default:
return new Date(a.createdAt).valueOf() - new Date(b.createdAt).valueOf();
}
});

const totalPages = Math.ceil(filtered.length / perPage);
Expand All @@ -200,7 +222,10 @@ export function useWorkers({
return {
page: normalizedPage,
totalPages: Math.floor(filtered.length / perPage),
workers: filtered.slice((normalizedPage - 1) * perPage, normalizedPage * perPage),
workers: sortWorkers(filtered, sortBy, sortDir).slice(
(normalizedPage - 1) * perPage,
normalizedPage * perPage,
),
};
}, [data?.workers, search, sortBy, sortDir, page, perPage]);

Expand All @@ -210,7 +235,7 @@ export function useWorkers({
};
}

export function useMyWorkers() {
export function useMyWorkers({ sortBy, sortDir }: { sortBy: WorkerSortBy; sortDir: SortDir }) {
const datasource = useSquidDataSource();
const { address } = useAccount();
const { isPending: isSettingsLoading } = useNetworkSettings();
Expand All @@ -224,15 +249,22 @@ export function useMyWorkers() {
{
select: res => {
return res.workers.map(w => {
return w;
return {
...w,
totalReward: BigNumber(w.claimedReward).plus(w.claimableReward).toFixed(),
};
});
},
enabled,
},
);

const workers = useMemo(() => {
return sortWorkers(data || [], sortBy, sortDir);
}, [data, sortBy, sortDir]);

return {
data: data || [],
data: workers,
isLoading: enabled ? isSettingsLoading || isLoading : false,
};
}
Expand Down Expand Up @@ -334,45 +366,66 @@ export function useMyClaimsAvailable({ source }: { source?: string } = {}) {
};
}

export function useMyDelegations() {
export function useMyDelegations({ sortBy, sortDir }: { sortBy: WorkerSortBy; sortDir: SortDir }) {
const { address } = useAccount();
const { isPending: isSettingsLoading } = useNetworkSettings();
const datasource = useSquidDataSource();

const { data, isLoading } = useMyDelegationsQuery(datasource, {
address: address || '',
});
const { data, isLoading } = useMyDelegationsQuery(
datasource,
{
address: address || '',
},
{
select: res => {
type W = SimplifyDeep<
MyDelegationsQuery['delegations'][number]['worker'] &
Pick<
WorkerExtended,
'delegationCapacity' | 'myDelegation' | 'myTotalDelegationReward'
> & {
delegations: Omit<MyDelegationsQuery['delegations'][number], 'worker'>[];
}
>;

const workers: Map<string, W> = new Map();
res?.delegations.map(d => {
let worker = workers.get(d.worker.id);
if (!worker) {
worker = {
...d.worker,
delegations: [],
delegationCapacity: calculateDelegationCapacity({
totalDelegation: d.worker.totalDelegation,
capedDelegation: d.worker.capedDelegation,
}),
myDelegation: '0',
myTotalDelegationReward: '0',
};
workers.set(worker.id, worker);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const delegation = {
...d,
worker: undefined,
};
delete delegation['worker'];

worker.myDelegation = BigNumber(worker.myDelegation).plus(delegation.deposit).toFixed();
worker.myTotalDelegationReward = BigNumber(worker.myTotalDelegationReward)
.plus(delegation.claimableReward)
.plus(delegation.claimedReward)
.toFixed();
worker.delegations.push(delegation);
});
return [...workers.values()];
},
},
);

const workers = useMemo(() => {
type W = WorkerFragmentFragment &
Pick<WorkerExtended, 'delegationCapacity'> & {
delegations: Omit<MyDelegationsQuery['delegations'][number], 'worker'>[];
};

const workers: Map<string, W> = new Map();
data?.delegations.map(d => {
let worker = workers.get(d.worker.id);
if (!worker) {
worker = {
...d.worker,
delegations: [],
delegationCapacity: calculateDelegationCapacity({
totalDelegation: d.worker.totalDelegation,
capedDelegation: d.worker.capedDelegation,
}),
};
workers.set(worker.id, worker);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const delegation = {
...d,
worker: undefined,
};
delete delegation['worker'];
worker.delegations.push(delegation);
});
return [...workers.values()];
}, [data]);
return sortWorkers(data || [], sortBy, sortDir);
}, [data, sortBy, sortDir]);

return {
isLoading: isSettingsLoading || isLoading,
Expand Down
88 changes: 55 additions & 33 deletions src/pages/DelegationsPage/DelegationsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { useMemo } from 'react';

import { percentFormatter, tokenFormatter } from '@lib/formatters/formatters.ts';
import { fromSqd } from '@lib/network';
import { Box, Stack, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
import BigNumber from 'bignumber.js';
import { keyBy, mapValues } from 'lodash-es';
import { Outlet } from 'react-router-dom';

import { useMyDelegations } from '@api/subsquid-network-squid';
import { SortDir, useMyDelegations, WorkerSortBy } from '@api/subsquid-network-squid';
import { Card } from '@components/Card';
import { Loader } from '@components/Loader';
import { BorderedTable } from '@components/Table/BorderedTable';
import { BorderedTable, SortableHeaderCell } from '@components/Table/BorderedTable';
import { Location, useLocationState } from '@hooks/useLocationState';
import { CenteredPageWrapper, NetworkPageTitle } from '@layouts/NetworkLayout';
import { ConnectedWalletRequired } from '@network/ConnectedWalletRequired';
import { useContracts } from '@network/useContracts';
Expand All @@ -21,27 +18,34 @@ import { WorkerStatus } from '@pages/WorkersPage/WorkerStatus';
import { WorkerUndelegate } from '@pages/WorkersPage/WorkerUndelegate';

export function MyDelegations() {
const { workers: delegations, isLoading } = useMyDelegations();
const [query, setQuery] = useLocationState({
sortBy: new Location.Enum<WorkerSortBy>(WorkerSortBy.MyDelegationReward),
sortDir: new Location.Enum<SortDir>(SortDir.Desc),
});
const { workers: delegations, isLoading } = useMyDelegations({
sortBy: query.sortBy as WorkerSortBy,
sortDir: query.sortDir as SortDir,
});
const { SQD_TOKEN } = useContracts();

const groupedDelegations = useMemo(() => {
return mapValues(
keyBy(delegations, w => w.id),
w => {
return w.delegations.reduce(
(s, d) => {
s.deposit = s.deposit.plus(d.deposit);
s.reward = s.reward.plus(d.claimedReward).plus(d.claimableReward);
return s;
},
{
deposit: new BigNumber(0),
reward: new BigNumber(0),
},
);
},
);
}, [delegations]);
// const groupedDelegations = useMemo(() => {
// return mapValues(
// keyBy(delegations, w => w.id),
// w => {
// return w.delegations.reduce(
// (s, d) => {
// s.deposit = s.deposit.plus(d.deposit);
// s.reward = s.reward.plus(d.claimedReward).plus(d.claimableReward);
// return s;
// },
// {
// deposit: new BigNumber(0),
// reward: new BigNumber(0),
// },
// );
// },
// );
// }, [delegations]);

return (
<Box>
Expand All @@ -57,10 +61,30 @@ export function MyDelegations() {
Worker
</TableCell>
<TableCell>Status</TableCell>
<TableCell>Delegator APR</TableCell>
<TableCell>Delegation capacity</TableCell>
<TableCell>My Delegation</TableCell>
<TableCell>Total reward</TableCell>
<SortableHeaderCell sort={WorkerSortBy.StakerAPR} query={query} setQuery={setQuery}>
Delegator APR
</SortableHeaderCell>
<SortableHeaderCell
sort={WorkerSortBy.DelegationCapacity}
query={query}
setQuery={setQuery}
>
Delegation capacity
</SortableHeaderCell>
<SortableHeaderCell
sort={WorkerSortBy.MyDelegation}
query={query}
setQuery={setQuery}
>
My Delegation
</SortableHeaderCell>
<SortableHeaderCell
sort={WorkerSortBy.MyDelegationReward}
query={query}
setQuery={setQuery}
>
Total reward
</SortableHeaderCell>
<TableCell className="pinned"></TableCell>
</TableRow>
</TableHead>
Expand All @@ -80,11 +104,9 @@ export function MyDelegations() {
<TableCell>
<DelegationCapacity worker={worker} />
</TableCell>
<TableCell>{tokenFormatter(fromSqd(worker.myDelegation), SQD_TOKEN)}</TableCell>
<TableCell>
{tokenFormatter(fromSqd(groupedDelegations[worker.id].deposit), SQD_TOKEN)}
</TableCell>
<TableCell>
{tokenFormatter(fromSqd(groupedDelegations[worker.id].reward), SQD_TOKEN)}
{tokenFormatter(fromSqd(worker.myTotalDelegationReward), SQD_TOKEN)}
</TableCell>
<TableCell className="pinned">
<Stack direction="row" spacing={2} justifyContent="flex-end">
Expand Down
Loading

0 comments on commit 3a6c7ef

Please sign in to comment.