Skip to content

Commit 9b47c9d

Browse files
authored
feat: liquidation rebates (#2058)
1 parent 7ac3c0c commit 9b47c9d

File tree

9 files changed

+486
-28
lines changed

9 files changed

+486
-28
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@cosmjs/tendermint-rpc": "^0.32.1",
5656
"@datadog/browser-logs": "^5.23.3",
5757
"@dydxprotocol/v4-client-js": "3.4.0",
58-
"@dydxprotocol/v4-localization": "1.1.377",
58+
"@dydxprotocol/v4-localization": "1.1.378",
5959
"@dydxprotocol/v4-proto": "^7.0.0-dev.0",
6060
"@emotion/is-prop-valid": "^1.3.0",
6161
"@hugocxl/react-to-image": "^0.0.9",

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants/statsig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export enum StatsigFlags {
2020
ffHideMarketsFilter = 'ff_hide_markets_filter',
2121
ffOpenInterestFilter = 'ff_open_interest_filter',
2222
abPopupDeposit = 'ab_popup_deposit',
23+
ffOnlyShowLiquidationRebates = 'ff_only_show_liquidation_rebates',
2324
}
2425

2526
export enum CustomFlags {

src/hooks/rewards/hooks.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,40 @@ async function getDydxFeeLeaderboard({ address }: { address?: string }) {
115115
addressEntry: data.addressEntry,
116116
};
117117
}
118+
119+
export type LiquidationLeaderboardItem = {
120+
address: string;
121+
total_liquidation_losses: string;
122+
rank: number;
123+
};
124+
125+
type LiquidationLeaderboardResponse = {
126+
success: boolean;
127+
data: LiquidationLeaderboardItem[];
128+
pagination?: {
129+
total: number;
130+
totalPages: number;
131+
page: number;
132+
perPage: number;
133+
};
134+
};
135+
136+
async function getLiquidationLeaderboard() {
137+
const res = await fetch(
138+
`https://pp-external-api-ffb2ad95ef03.herokuapp.com/api/dydx-liquidation-leaderboard?perPage=1000`
139+
);
140+
141+
const data = (await res.json()) as LiquidationLeaderboardResponse;
142+
return data.data;
143+
}
144+
145+
export function useLiquidationLeaderboard() {
146+
return useQuery({
147+
queryKey: ['dydx-liquidation-leaderboard'],
148+
queryFn: wrapAndLogError(
149+
() => getLiquidationLeaderboard(),
150+
'LaunchIncentives/fetchLiquidationLeaderboard',
151+
true
152+
),
153+
});
154+
}

src/hooks/rewards/util.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export const CURRENT_SURGE_REWARDS_DETAILS = {
3333
endTime: '2026-01-31T23:59:59.000Z', // end of jan 2026
3434
};
3535

36+
export const LIQUIDATION_REBATES_DETAILS = {
37+
rebateAmount: '$1M',
38+
rebateAmountUsd: 1_000_000,
39+
};
40+
3641
export const DEC_2025_COMPETITION_DETAILS = {
3742
rewardAmount: '$1M',
3843
rewardAmountUsd: 1_000_000,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StatsigFlags } from '@/constants/statsig';
2+
3+
import { useStatsigGateValue } from './useStatsig';
4+
5+
export const useEnableLiquidationRebates = () => {
6+
const onlyShowLiquidationRebatesFF = useStatsigGateValue(
7+
StatsigFlags.ffOnlyShowLiquidationRebates
8+
);
9+
return onlyShowLiquidationRebatesFF;
10+
};
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { useEffect, useMemo, useState } from 'react';
2+
3+
import { Duration } from 'luxon';
4+
import tw from 'twin.macro';
5+
6+
import { STRING_KEYS } from '@/constants/localization';
7+
8+
import { LIQUIDATION_REBATES_DETAILS } from '@/hooks/rewards/util';
9+
import { useNow } from '@/hooks/useNow';
10+
import { useStringGetter } from '@/hooks/useStringGetter';
11+
12+
import { Icon, IconName } from '@/components/Icon';
13+
import { Link } from '@/components/Link';
14+
import { Panel } from '@/components/Panel';
15+
import { SuccessTag, TagSize } from '@/components/Tag';
16+
17+
export const LiquidationRebatesHeader = () => {
18+
const stringGetter = useStringGetter();
19+
20+
// Calculate the last millisecond of the current UTC month
21+
const now = new Date();
22+
const endOfCurrentMonth = (() => {
23+
const date = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0)); // first ms of next month
24+
date.setTime(date.getTime() - 1); // last ms of this month
25+
return date.toISOString();
26+
})();
27+
28+
return (
29+
<$Panel>
30+
<div tw="flex gap-3 pb-0.25 pt-0.5">
31+
<div tw="flex flex-col gap-1.5">
32+
<div tw="flex flex-col gap-0.5">
33+
<div tw="flex items-center gap-0.5">
34+
<div tw="font-medium-bold">
35+
<span tw="font-bold">
36+
{stringGetter({
37+
key: STRING_KEYS.LIQUIDATION_REBATES_HEADLINE,
38+
params: {
39+
REBATE_AMOUNT: LIQUIDATION_REBATES_DETAILS.rebateAmount,
40+
},
41+
})}
42+
</span>
43+
</div>
44+
<SuccessTag size={TagSize.Medium}>
45+
{stringGetter({ key: STRING_KEYS.ACTIVE })}
46+
</SuccessTag>
47+
</div>
48+
<div>
49+
<p tw="mb-0.5 text-color-text-0">
50+
{stringGetter({
51+
key: STRING_KEYS.LIQUIDATION_REBATES_BODY,
52+
})}
53+
</p>
54+
<p tw="text-color-text-0">
55+
{stringGetter({
56+
key: STRING_KEYS.LIQUIDATION_REBATES_SUB_BODY,
57+
params: {
58+
LOSS_REBATES_LINK: (
59+
<Link
60+
href="https://dydx.forum/t/drc-realized-losses-rebate-pilot-program/4828/2"
61+
isInline
62+
>
63+
{stringGetter({ key: STRING_KEYS.LOSS_REBATES })}
64+
</Link>
65+
),
66+
CHECK_ELIGIBILITY_LINK: (
67+
<Link href="https://www.dydx.xyz/liquidation-rebates" isInline>
68+
{stringGetter({ key: STRING_KEYS.HERE })}
69+
</Link>
70+
),
71+
},
72+
})}
73+
</p>
74+
</div>
75+
</div>
76+
<div tw="flex items-center gap-0.25 self-start rounded-3 bg-color-layer-1 px-0.875 py-0.5">
77+
<Icon iconName={IconName.Clock} size="1.25rem" tw="text-color-accent" />
78+
<div tw="flex gap-0.375">
79+
<div tw="text-color-accent">
80+
{stringGetter({
81+
key: STRING_KEYS.MONTH_COUNTDOWN,
82+
})}
83+
:
84+
</div>
85+
{/* Countdown to end of current month */}
86+
<MinutesCountdown endTime={endOfCurrentMonth} />
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
</$Panel>
92+
);
93+
};
94+
95+
const MinutesCountdown = ({ endTime }: { endTime: string }) => {
96+
const targetMs = Date.parse(endTime);
97+
const now = useNow();
98+
const [msLeft, setMsLeft] = useState(Math.max(0, Math.floor(targetMs - Date.now())));
99+
100+
useEffect(() => {
101+
if (now > targetMs) {
102+
return;
103+
}
104+
105+
const newMsLeft = Math.max(0, Math.floor(targetMs - now));
106+
setMsLeft(newMsLeft);
107+
}, [now, targetMs]);
108+
109+
const formattedMsLeft = useMemo(() => {
110+
return Duration.fromMillis(msLeft)
111+
.shiftTo('days', 'hours', 'minutes', 'seconds')
112+
.toFormat("d'd' h'h' m'm' s's'", { floor: true });
113+
}, [msLeft]);
114+
115+
return <div>{formattedMsLeft}</div>;
116+
};
117+
118+
const $Panel = tw(Panel)`bg-color-layer-3 w-full`;

0 commit comments

Comments
 (0)