Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 6857b62

Browse files
jonyburCopilot
andauthored
feat: implements BABY rewards card and function (#1362)
* implements baby rewards * Remove debug logs * Update src/ui/baby/components/RewardCard/index.tsx Co-authored-by: Copilot <[email protected]> * Fix claims * Revisions * Fix rewards modal --------- Co-authored-by: Copilot <[email protected]>
1 parent 45543dc commit 6857b62

File tree

7 files changed

+212
-47
lines changed

7 files changed

+212
-47
lines changed

src/ui/baby/components/RewardCard/index.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { Avatar, Button, SubSection, Text } from "@babylonlabs-io/core-ui";
22

3+
import { useRewardState } from "@/ui/baby/state/RewardState";
34
import { getNetworkConfigBBN } from "@/ui/common/config/network/bbn";
5+
import { ubbnToBaby } from "@/ui/common/utils/bbn";
6+
import { maxDecimals } from "@/ui/common/utils/maxDecimals";
47

58
const { logo, coinSymbol } = getNetworkConfigBBN();
69

710
export function RewardCard() {
11+
const { totalReward, openClaimModal, loading, refreshRewards } =
12+
useRewardState();
13+
14+
const hasRewards = totalReward > 0n;
15+
const formattedReward = maxDecimals(ubbnToBaby(Number(totalReward)), 6);
16+
817
return (
918
<SubSection className="flex-col gap-4">
1019
<div className="flex justify-between items-center w-full">
@@ -15,7 +24,24 @@ export function RewardCard() {
1524
</span>
1625
</div>
1726

18-
<Button size="small">Claim</Button>
27+
<div className="flex gap-2">
28+
<Button
29+
size="small"
30+
onClick={refreshRewards}
31+
disabled={loading}
32+
variant="outlined"
33+
aria-label="Refresh rewards"
34+
>
35+
🔄
36+
</Button>
37+
<Button
38+
size="small"
39+
onClick={openClaimModal}
40+
disabled={!hasRewards || loading}
41+
>
42+
Claim
43+
</Button>
44+
</div>
1945
</div>
2046

2147
<Text
@@ -24,10 +50,17 @@ export function RewardCard() {
2450
className="flex justify-between items-center text-accent-secondary"
2551
>
2652
<span>Babylon Genesis</span>
27-
<span>$100.00 USD</span>
53+
<span>
54+
{formattedReward} {coinSymbol}
55+
<span style={{ fontSize: "10px", opacity: 0.5, marginLeft: "8px" }}>
56+
(raw: {totalReward.toString()})
57+
</span>
58+
</span>
2859
</Text>
2960

30-
<Button fluid>Claim</Button>
61+
<Button fluid onClick={openClaimModal} disabled={!hasRewards || loading}>
62+
Claim
63+
</Button>
3164
</SubSection>
3265
);
3366
}

src/ui/baby/components/RewardPreviewModal/index.tsx

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@ import {
55
DialogHeader,
66
Text,
77
} from "@babylonlabs-io/core-ui";
8-
import { PropsWithChildren } from "react";
8+
import { PropsWithChildren, useEffect, useState } from "react";
99

10+
import babylon from "@/infrastructure/babylon";
1011
import { ResponsiveDialog } from "@/ui/common/components/Modals/ResponsiveDialog";
1112
import { getNetworkConfigBBN } from "@/ui/common/config/network/bbn";
13+
import { useCosmosWallet } from "@/ui/common/context/wallet/CosmosWalletProvider";
14+
import { usePrice } from "@/ui/common/hooks/client/api/usePrices";
15+
import { useBbnTransaction } from "@/ui/common/hooks/client/rpc/mutation/useBbnTransaction";
16+
import { ubbnToBaby } from "@/ui/common/utils/bbn";
17+
import { calculateTokenValueInCurrency } from "@/ui/common/utils/formatCurrency";
18+
import { maxDecimals } from "@/ui/common/utils/maxDecimals";
1219

13-
const { coinSymbol } = getNetworkConfigBBN();
20+
const { coinSymbol, displayUSD } = getNetworkConfigBBN();
1421

1522
interface PreviewModalProps {
1623
open: boolean;
1724
processing?: boolean;
1825
title: string;
1926
onClose: () => void;
2027
onProceed: () => void;
28+
rewards: Array<{ validatorAddress: string }>;
29+
totalReward: bigint;
2130
}
2231

2332
export const RewardsPreviewModal = ({
@@ -26,7 +35,54 @@ export const RewardsPreviewModal = ({
2635
title,
2736
onClose,
2837
onProceed,
38+
rewards,
39+
totalReward,
2940
}: PropsWithChildren<PreviewModalProps>) => {
41+
const { bech32Address } = useCosmosWallet();
42+
const { estimateBbnGasFee } = useBbnTransaction();
43+
const babyPrice = usePrice(coinSymbol);
44+
const [feeAmount, setFeeAmount] = useState<number>(0);
45+
const [feeLoading, setFeeLoading] = useState<boolean>(false);
46+
47+
// Estimate fee when modal opens
48+
useEffect(() => {
49+
const estimateFee = async () => {
50+
if (!open || !bech32Address || !rewards.length) return;
51+
52+
setFeeLoading(true);
53+
try {
54+
const msgs = rewards.map((reward) =>
55+
babylon.txs.baby.createClaimRewardMsg({
56+
validatorAddress: reward.validatorAddress,
57+
delegatorAddress: bech32Address,
58+
}),
59+
);
60+
const gasFee = await estimateBbnGasFee(msgs);
61+
const totalFee = gasFee.amount.reduce(
62+
(acc, coin) => acc + Number(coin.amount),
63+
0,
64+
);
65+
setFeeAmount(totalFee);
66+
} catch (error) {
67+
console.error("Error estimating fee:", error);
68+
setFeeAmount(0);
69+
} finally {
70+
setFeeLoading(false);
71+
}
72+
};
73+
74+
estimateFee();
75+
}, [open, bech32Address, rewards, estimateBbnGasFee]);
76+
77+
const feeInBaby = ubbnToBaby(feeAmount);
78+
const feeInUsd = displayUSD
79+
? calculateTokenValueInCurrency(feeInBaby, babyPrice)
80+
: undefined;
81+
82+
const rewardInBaby = ubbnToBaby(Number(totalReward));
83+
const rewardInUsd = displayUSD
84+
? calculateTokenValueInCurrency(rewardInBaby, babyPrice)
85+
: undefined;
3086
return (
3187
<ResponsiveDialog open={open} onClose={onClose}>
3288
<DialogHeader title={title} className="text-accent-primary" />
@@ -36,8 +92,8 @@ export const RewardsPreviewModal = ({
3692
<Text variant="body1" className="flex justify-between">
3793
<span>Babylon Genesis</span>
3894
<span className="flex flex-col items-end">
39-
1000 {coinSymbol}
40-
<Text variant="body2">~ $5,677.39 USD</Text>
95+
{maxDecimals(rewardInBaby, 6)} {coinSymbol}
96+
{rewardInUsd && <Text variant="body2">{rewardInUsd}</Text>}
4197
</span>
4298
</Text>
4399

@@ -46,8 +102,14 @@ export const RewardsPreviewModal = ({
46102
<Text variant="body1" className="flex justify-between">
47103
<span>Transaction Fees</span>
48104
<span className="flex flex-col gap-2 items-end">
49-
10 {coinSymbol}
50-
<Text variant="body2">~ $5,677.39 USD</Text>
105+
{feeLoading ? (
106+
<Text variant="body2">Calculating...</Text>
107+
) : (
108+
<>
109+
{maxDecimals(feeInBaby, 6)} {coinSymbol}
110+
{feeInUsd && <Text variant="body2">{feeInUsd}</Text>}
111+
</>
112+
)}
51113
</span>
52114
</Text>
53115
</div>

src/ui/baby/hooks/services/useRewardService.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import babylon from "@/infrastructure/babylon";
44
import { useRewards } from "@/ui/baby/hooks/api/useRewards";
55
import { useCosmosWallet } from "@/ui/common/context/wallet/CosmosWalletProvider";
66
import { useBbnTransaction } from "@/ui/common/hooks/client/rpc/mutation/useBbnTransaction";
7+
import { coinAmountToBigInt } from "@/ui/common/utils/bbn";
78

89
export interface Reward {
910
validatorAddress: string;
@@ -20,27 +21,30 @@ export function useRewardService() {
2021
} = useRewards(bech32Address);
2122
const { signBbnTx, sendBbnTx } = useBbnTransaction();
2223

23-
const rewardList = useMemo(
24-
() =>
25-
rewards
26-
.map(({ validatorAddress, reward }) => {
27-
const coin = reward.find((coin) => coin.denom === "ubbn");
28-
return coin
29-
? {
30-
validatorAddress,
31-
amount: BigInt(coin.amount),
32-
coin: coin.denom,
33-
}
34-
: null;
35-
})
36-
.filter(Boolean) as Reward[],
37-
[rewards],
38-
);
24+
const rewardList = useMemo(() => {
25+
const processed = rewards
26+
.map(({ validatorAddress, reward }) => {
27+
const coin = reward.find((coin) => coin.denom === "ubbn");
28+
return coin
29+
? {
30+
validatorAddress,
31+
amount: coinAmountToBigInt(coin.amount),
32+
coin: coin.denom,
33+
}
34+
: null;
35+
})
36+
.filter(Boolean) as Reward[];
3937

40-
const totalReward = useMemo(
41-
() => rewardList.reduce((total, reward) => total + reward.amount, 0n),
42-
[rewardList],
43-
);
38+
return processed;
39+
}, [rewards]);
40+
41+
const totalReward = useMemo(() => {
42+
const total = rewardList.reduce(
43+
(total, reward) => total + reward.amount,
44+
0n,
45+
);
46+
return total;
47+
}, [rewardList]);
4448

4549
const claimAllRewards = useCallback(async () => {
4650
if (!bech32Address) throw Error("Babylon Wallet is not connected");
@@ -61,5 +65,6 @@ export function useRewardService() {
6165
rewards: rewardList,
6266
totalReward,
6367
claimAllRewards,
68+
refetchRewards,
6469
};
6570
}

src/ui/baby/layout.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Outlet } from "react-router";
22

33
import { DelegationState } from "@/ui/baby/state/DelegationState";
4+
import { RewardState } from "@/ui/baby/state/RewardState";
45
import { StakingState } from "@/ui/baby/state/StakingState";
56
import { ValidatorState } from "@/ui/baby/state/ValidatorState";
67
import { AuthGuard } from "@/ui/common/components/Common/AuthGuard";
@@ -12,19 +13,21 @@ export default function BabyLayout() {
1213
<StakingState>
1314
<ValidatorState>
1415
<DelegationState>
15-
<Content>
16-
<AuthGuard>
17-
<Nav>
18-
<NavItem title="Stake" to="/baby/staking" />
19-
<NavItem title="Activity" to="/baby/activities" />
20-
<NavItem title="Rewards" to="/baby/rewards" />
21-
</Nav>
22-
</AuthGuard>
16+
<RewardState>
17+
<Content>
18+
<AuthGuard>
19+
<Nav>
20+
<NavItem title="Stake" to="/baby/staking" />
21+
<NavItem title="Activity" to="/baby/activities" />
22+
<NavItem title="Rewards" to="/baby/rewards" />
23+
</Nav>
24+
</AuthGuard>
2325

24-
<div className="mt-10">
25-
<Outlet />
26-
</div>
27-
</Content>
26+
<div className="mt-10">
27+
<Outlet />
28+
</div>
29+
</Content>
30+
</RewardState>
2831
</DelegationState>
2932
</ValidatorState>
3033
</StakingState>

src/ui/baby/state/RewardState.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,55 @@ import { createStateUtils } from "@/ui/common/utils/createStateUtils";
1111
interface RewardState {
1212
loading: boolean;
1313
rewards: Reward[];
14+
totalReward: bigint;
1415
claimAll: () => Promise<void>;
16+
showClaimModal: boolean;
17+
openClaimModal: () => void;
18+
closeClaimModal: () => void;
19+
refreshRewards: () => void;
1520
}
1621

1722
const { StateProvider, useState: useRewardState } =
1823
createStateUtils<RewardState>({
1924
loading: false,
2025
rewards: [],
26+
totalReward: 0n,
2127
claimAll: async () => {},
28+
showClaimModal: false,
29+
openClaimModal: () => {},
30+
closeClaimModal: () => {},
31+
refreshRewards: () => {},
2232
});
2333

2434
function RewardState({ children }: PropsWithChildren) {
2535
const [processing, setProcessing] = useState(false);
36+
const [showClaimModal, setShowClaimModal] = useState(false);
2637

27-
const { loading, rewards, totalReward, claimAllRewards } = useRewardService();
38+
const { loading, rewards, totalReward, claimAllRewards, refetchRewards } =
39+
useRewardService();
2840
const { handleError } = useError();
2941
const logger = useLogger();
3042

43+
const openClaimModal = useCallback(() => {
44+
setShowClaimModal(true);
45+
}, []);
46+
47+
const closeClaimModal = useCallback(() => {
48+
setShowClaimModal(false);
49+
}, []);
50+
51+
const refreshRewards = useCallback(() => {
52+
refetchRewards();
53+
}, [refetchRewards]);
54+
3155
const claimAll = useCallback(async () => {
3256
try {
3357
setProcessing(true);
3458
const result = await claimAllRewards();
35-
logger.info("Baby Staking: stake", {
59+
logger.info("Baby Staking: claim rewards", {
3660
txHash: result?.txHash,
3761
});
62+
setShowClaimModal(false);
3863
} catch (error: any) {
3964
handleError({ error });
4065
logger.error(error);
@@ -49,8 +74,22 @@ function RewardState({ children }: PropsWithChildren) {
4974
rewards,
5075
totalReward,
5176
claimAll,
77+
showClaimModal,
78+
openClaimModal,
79+
closeClaimModal,
80+
refreshRewards,
5281
}),
53-
[loading, processing, rewards, totalReward, claimAll],
82+
[
83+
loading,
84+
processing,
85+
rewards,
86+
totalReward,
87+
claimAll,
88+
showClaimModal,
89+
openClaimModal,
90+
closeClaimModal,
91+
refreshRewards,
92+
],
5493
);
5594

5695
return <StateProvider value={context}>{children}</StateProvider>;

0 commit comments

Comments
 (0)