Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/young-dingos-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": minor
---

Add the wallets tab to the Stats page
2 changes: 2 additions & 0 deletions apps/evm/src/clients/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,7 @@ export * from './queries/getRiskDashboardWalletAggregates';
export * from './queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates';
export * from './queries/getRiskDashboardTopWallets';
export * from './queries/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets';
export * from './queries/getRiskDashboardWallets';
export * from './queries/getRiskDashboardWallets/useGetRiskDashboardWallets';
export * from './queries/getRiskDashboardTransactionsVolume';
export * from './queries/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume';
98 changes: 98 additions & 0 deletions apps/evm/src/clients/api/queries/getRiskDashboardWallets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { VError } from 'libs/errors';
import type { ChainId } from 'types';
import { restService } from 'utilities';
import type { ApiRiskDashboardAsOf } from '../getRiskDashboardMarketAggregates';
import type { ApiRiskDashboardTopWalletPosition } from '../getRiskDashboardTopWallets';

export type RiskDashboardWalletsOrderBy =
| 'supply'
| 'collateral'
| 'borrow'
| 'healthFactor'
| 'badDebt'
| 'badDebtDuration';

export type RiskDashboardWalletsRiskStatus = 'at_risk' | 'eligible_for_liquidation' | 'bad_debt';

export interface ApiRiskDashboardWallet {
address: string;
totalSupplyUsdCents: string;
totalCollateralUsdCents: string;
totalBorrowUsdCents: string;
healthFactorMantissa: string;
badDebtUsdCents: string;
badDebtStartedAt: string | null;
badDebtStartedAtIsFloor: boolean;
positions: ApiRiskDashboardTopWalletPosition[];
}

export interface GetRiskDashboardWalletsInput {
chainId: ChainId;
page: number;
limit: number;
orderBy: RiskDashboardWalletsOrderBy;
order: 'asc' | 'desc';
riskStatus?: RiskDashboardWalletsRiskStatus;
minPositionUsdCents?: number;
marketAddresses?: string[];
suppliedMarketAddresses?: string[];
borrowedMarketAddresses?: string[];
}

export interface GetRiskDashboardWalletsResponse {
chainId: string;
asOf: ApiRiskDashboardAsOf | null;
page: number;
limit: number;
total: number;
wallets: ApiRiskDashboardWallet[];
}

const serializeAddresses = (addresses?: string[]) =>
addresses && addresses.length > 0 ? addresses.join(',') : undefined;

export async function getRiskDashboardWallets({
chainId,
page,
limit,
orderBy,
order,
riskStatus,
minPositionUsdCents,
marketAddresses,
suppliedMarketAddresses,
borrowedMarketAddresses,
}: GetRiskDashboardWalletsInput) {
const response = await restService<GetRiskDashboardWalletsResponse>({
endpoint: '/risk-dashboard/wallets',
method: 'GET',
params: {
chainId,
page,
limit,
orderBy,
order,
riskStatus,
minPositionUsdCents,
marketAddresses: serializeAddresses(marketAddresses),
suppliedMarketAddresses: serializeAddresses(suppliedMarketAddresses),
borrowedMarketAddresses: serializeAddresses(borrowedMarketAddresses),
},
});

const payload = response.data;

if (payload && 'error' in payload) {
throw new VError({
type: 'unexpected',
code: 'somethingWentWrong',
data: { exception: payload.error },
});
}

if (!payload) {
throw new VError({ type: 'unexpected', code: 'somethingWentWrong' });
}

return payload;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type QueryObserverOptions, useQuery } from '@tanstack/react-query';

import FunctionKey from 'constants/functionKey';
import { useChainId } from 'libs/wallet';
import type { ChainId } from 'types';
import {
type GetRiskDashboardWalletsInput,
type GetRiskDashboardWalletsResponse,
getRiskDashboardWallets,
} from '.';

export type UseGetRiskDashboardWalletsInput = Omit<GetRiskDashboardWalletsInput, 'chainId'>;

export type UseGetRiskDashboardWalletsQueryKey = [
FunctionKey.GET_RISK_DASHBOARD_WALLETS,
UseGetRiskDashboardWalletsInput & { chainId: ChainId },
];

type Options = QueryObserverOptions<
GetRiskDashboardWalletsResponse,
Error,
GetRiskDashboardWalletsResponse,
GetRiskDashboardWalletsResponse,
UseGetRiskDashboardWalletsQueryKey
>;

export const useGetRiskDashboardWallets = (
input: UseGetRiskDashboardWalletsInput,
options?: Partial<Options>,
) => {
const { chainId } = useChainId();

return useQuery({
queryKey: [FunctionKey.GET_RISK_DASHBOARD_WALLETS, { chainId, ...input }],
queryFn: () => getRiskDashboardWallets({ chainId, ...input }),
...options,
});
};
1 change: 1 addition & 0 deletions apps/evm/src/constants/functionKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ enum FunctionKey {
GET_RISK_DASHBOARD_MARKET_SNAPSHOTS = 'GET_RISK_DASHBOARD_MARKET_SNAPSHOTS',
GET_RISK_DASHBOARD_WALLET_AGGREGATES = 'GET_RISK_DASHBOARD_WALLET_AGGREGATES',
GET_RISK_DASHBOARD_TOP_WALLETS = 'GET_RISK_DASHBOARD_TOP_WALLETS',
GET_RISK_DASHBOARD_WALLETS = 'GET_RISK_DASHBOARD_WALLETS',
GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME = 'GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME',
GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY = 'GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY',
GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY = 'GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY',
Expand Down
26 changes: 26 additions & 0 deletions apps/evm/src/libs/translations/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2062,6 +2062,32 @@
"unavailable": "Wallet data unavailable.",
"valueEligibleForLiquidation": "Value Eligible for Liquidation",
"walletsAtRisk": "Wallets at Risk"
},
"walletsTable": {
"columns": {
"badDebt": "Bad Debt",
"badDebtDuration": "Bad Debt Duration",
"borrow": "Borrow",
"collateral": "Collateral",
"healthFactor": "Health Factor",
"supply": "Supply",
"suppliedAssets": "Supplied Assets",
"wallet": "Wallet"
},
"filters": {
"borrowedAssets": "Borrowed assets",
"market": "Market",
"minPosition": "Min position",
"riskStatus": "Risk status",
"suppliedAssets": "Supplied assets"
},
"noData": "No wallets match the selected filters.",
"riskStatus": {
"all": "All",
"atRisk": "At risk",
"badDebt": "Bad debt",
"eligibleForLiquidation": "Eligible for liquidation"
}
}
},
"swap": {
Expand Down
74 changes: 74 additions & 0 deletions apps/evm/src/pages/Stats/Wallets/AssetMultiSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { cn } from '@venusprotocol/ui';

import { Checkbox, Dropdown } from 'components';

export interface AssetMultiSelectOption {
value: string;
label: string;
iconSrc?: string;
}

export interface AssetMultiSelectProps {
label: string;
options: AssetMultiSelectOption[];
selectedValues: string[];
onChange: (selectedValues: string[]) => void;
className?: string;
}

export const AssetMultiSelect: React.FC<AssetMultiSelectProps> = ({
label,
options,
selectedValues,
onChange,
className,
}) => {
const toggleValue = (value: string) => {
if (selectedValues.includes(value)) {
onChange(selectedValues.filter(selected => selected !== value));
return;
}
onChange([...selectedValues, value]);
};

const summary = selectedValues.length > 0 ? `${label} (${selectedValues.length})` : label;

return (
<Dropdown
className={className}
menuTitle={label}
optionsDom={() => (
<div className="max-h-72 overflow-y-auto py-1">
{options.map(option => (
<label
key={option.value}
className="flex cursor-pointer items-center gap-x-2 px-3 py-2 hover:bg-lightGrey/10"
>
<Checkbox
value={selectedValues.includes(option.value)}
onChange={() => toggleValue(option.value)}
/>

{option.iconSrc && <img alt="" src={option.iconSrc} className="size-5" />}

<span className="text-b2 text-white">{option.label}</span>
</label>
))}
</div>
)}
>
{({ handleToggleDropdown }) => (
<button
type="button"
onClick={handleToggleDropdown}
className={cn(
'flex h-10 items-center gap-x-2 rounded-lg border border-lightGrey px-3 text-b2',
selectedValues.length > 0 ? 'text-white' : 'text-grey',
)}
>
{summary}
</button>
)}
</Dropdown>
);
};
Loading
Loading