diff --git a/src/ui/common/components/Rewards/Rewards.tsx b/src/ui/common/components/Rewards/Rewards.tsx index f774a9370..893050cdb 100644 --- a/src/ui/common/components/Rewards/Rewards.tsx +++ b/src/ui/common/components/Rewards/Rewards.tsx @@ -8,6 +8,7 @@ import babyTokenIcon from "@/ui/common/assets/baby-token.svg"; import { AuthGuard } from "@/ui/common/components/Common/AuthGuard"; import { Section } from "@/ui/common/components/Section/Section"; import { getNetworkConfigBBN } from "@/ui/common/config/network/bbn"; +import { useBbnQuery } from "@/ui/common/hooks/client/rpc/queries/useBbnQuery"; import { useRewardsService } from "@/ui/common/hooks/services/useRewardsService"; import { useRewardsState } from "@/ui/common/state/RewardState"; import { ubbnToBaby } from "@/ui/common/utils/bbn"; @@ -54,39 +55,136 @@ export function Rewards() { } = useRewardsState(); const { showPreview, claimRewards } = useRewardsService(); + const { rewardCoinsQuery } = useBbnQuery(); const { networkName: bbnNetworkName, coinSymbol: bbnCoinSymbol } = getNetworkConfigBBN(); - // BABY / tBABY reward + // Build rewards list from per-denom rewards; fallback to BABY only if empty const formattedRewardBaby = rewardBalance ? ubbnToBaby(rewardBalance).toString() : "0"; const babyIcon = /BABY$/i.test(bbnCoinSymbol) ? babyTokenIcon : generatePlaceholder(bbnCoinSymbol.charAt(0)); - const rewards: RewardItem[] = [ - { - amount: formattedRewardBaby, - currencyIcon: babyIcon, - chainName: bbnNetworkName, - currencyName: bbnCoinSymbol, - placeholder: "0", - displayBalance: true, - balanceDetails: { - balance: formattedRewardBaby, - symbol: bbnCoinSymbol, - price: 0, - displayUSD: false, - decimals: 6, - }, - }, - ]; + + const rewards: RewardItem[] = (() => { + const coins = rewardCoinsQuery.data ?? []; + console.log("[RewardsUI] rewardCoinsQuery", { + loading: rewardCoinsQuery.isLoading, + error: rewardCoinsQuery.error, + data: coins, + }); + if (!coins.length) { + return [ + { + amount: formattedRewardBaby, + currencyIcon: babyIcon, + chainName: bbnNetworkName, + currencyName: bbnCoinSymbol, + placeholder: "0", + displayBalance: true, + balanceDetails: { + balance: formattedRewardBaby, + symbol: bbnCoinSymbol, + price: 0, + displayUSD: false, + decimals: 6, + }, + }, + ]; + } + + return coins.map(({ denom, amount }) => { + console.log("[RewardsUI] processing denom", { denom, amount }); + if (denom === "ubbn") { + const amt = ubbnToBaby(amount).toString(); + console.log("[RewardsUI] ubbn decoded", { amount, baby: amt }); + return { + amount: amt, + currencyIcon: babyIcon, + chainName: bbnNetworkName, + currencyName: bbnCoinSymbol, + placeholder: "0", + displayBalance: true, + balanceDetails: { + balance: amt, + symbol: bbnCoinSymbol, + price: 0, + displayUSD: false, + decimals: 6, + }, + } as RewardItem; + } + + if (denom.startsWith("factory/")) { + const parts = denom.split("/"); + const subdenom = parts[parts.length - 1] || denom; + console.log("[RewardsUI] factory token", { denom, subdenom }); + return { + amount: String(amount), + currencyIcon: generatePlaceholder(subdenom.charAt(0).toUpperCase()), + chainName: bbnNetworkName, + currencyName: subdenom, + placeholder: "0", + displayBalance: true, + balanceDetails: { + balance: String(amount), + symbol: subdenom, + price: 0, + displayUSD: false, + decimals: 0, + }, + } as RewardItem; + } + + // IBC token: show denom text for now (hash) + if (denom.startsWith("ibc/")) { + const symbol = denom; // minimal: show denom text + console.log("[RewardsUI] ibc token", { denom }); + return { + amount: String(amount), + currencyIcon: generatePlaceholder("I"), + chainName: bbnNetworkName, + currencyName: symbol, + placeholder: "0", + displayBalance: true, + balanceDetails: { + balance: String(amount), + symbol, + price: 0, + displayUSD: false, + decimals: 0, + }, + } as RewardItem; + } + + // Fallback: show raw denom + const symbol = denom; + console.log("[RewardsUI] unknown denom fallback", { denom }); + return { + amount: String(amount), + currencyIcon: generatePlaceholder(symbol.charAt(0).toUpperCase()), + chainName: bbnNetworkName, + currencyName: symbol, + placeholder: "0", + displayBalance: true, + balanceDetails: { + balance: String(amount), + symbol, + price: 0, + displayUSD: false, + decimals: 0, + }, + } as RewardItem; + }); + })(); const [previewOpen, setPreviewOpen] = useState(false); const handleClick = async () => { - if (!rewardBalance || processing) return; + const hasAnyRewards = (rewardCoinsQuery.data?.length ?? 0) > 0; + if ((!hasAnyRewards && !rewardBalance) || processing) return; await showPreview(); setPreviewOpen(true); }; @@ -100,7 +198,9 @@ export function Rewards() { setPreviewOpen(false); }; - const claimDisabled = !rewardBalance || processing; + const claimDisabled = + processing || + ((rewardCoinsQuery.data?.length ?? 0) === 0 && !rewardBalance); return ( diff --git a/src/ui/common/hooks/client/rpc/queries/useBbnQuery.ts b/src/ui/common/hooks/client/rpc/queries/useBbnQuery.ts index 7163d65c3..d72930e5c 100644 --- a/src/ui/common/hooks/client/rpc/queries/useBbnQuery.ts +++ b/src/ui/common/hooks/client/rpc/queries/useBbnQuery.ts @@ -21,6 +21,7 @@ import { useRpcErrorHandler } from "../useRpcErrorHandler"; const BBN_BTCLIGHTCLIENT_TIP_KEY = "BBN_BTCLIGHTCLIENT_TIP"; const BBN_BALANCE_KEY = "BBN_BALANCE"; const BBN_REWARDS_KEY = "BBN_REWARDS"; +const BBN_REWARDS_COINS_KEY = "BBN_REWARDS_COINS"; const BBN_HEIGHT_KEY = "BBN_HEIGHT"; const REWARD_GAUGE_KEY_BTC_DELEGATION = "BTC_STAKER"; @@ -73,15 +74,18 @@ export const useBbnQuery = () => { return 0; } - const coins = - rewards.rewardGauges[REWARD_GAUGE_KEY_BTC_DELEGATION]?.coins; + const coins = rewards.rewardGauges[ + REWARD_GAUGE_KEY_BTC_DELEGATION + ]?.coins?.filter((c) => c.denom === "ubbn"); if (!coins) { return 0; } const withdrawnCoins = rewards.rewardGauges[ REWARD_GAUGE_KEY_BTC_DELEGATION - ]?.withdrawnCoins.reduce((acc, coin) => acc + Number(coin.amount), 0); + ]?.withdrawnCoins + .filter((c) => c.denom === "ubbn") + .reduce((acc, coin) => acc + Number(coin.amount), 0); return ( coins.reduce((acc, coin) => acc + Number(coin.amount), 0) - @@ -99,6 +103,96 @@ export const useBbnQuery = () => { refetchInterval: ONE_MINUTE, }); + /** + * Gets per-denom rewards coins for BTC_STAKER gauge. + * Returns an array of { denom: string; amount: number } with withdrawn amounts subtracted. + */ + const rewardCoinsQuery = useClientQuery({ + queryKey: [BBN_REWARDS_COINS_KEY, bech32Address, connected], + queryFn: async () => { + if (!connected || !queryClient || !bech32Address) { + return [] as Array<{ denom: string; amount: number }>; + } + console.log("[BbnQuery] Fetching per-denom reward coins", { + address: bech32Address, + }); + const { incentive } = setupIncentiveExtension(queryClient); + const req: incentivequery.QueryRewardGaugesRequest = + incentivequery.QueryRewardGaugesRequest.fromPartial({ + address: bech32Address, + }); + + let rewards: incentivequery.QueryRewardGaugesResponse; + try { + rewards = await incentive.RewardGauges(req); + console.log("[BbnQuery] Raw RewardGauges response", rewards); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("reward gauge not found") + ) { + console.log( + "[BbnQuery] Reward gauge not found; returning empty rewards", + ); + return [] as Array<{ denom: string; amount: number }>; + } + throw new ClientError( + ERROR_CODES.EXTERNAL_SERVICE_UNAVAILABLE, + "Error getting rewards", + { cause: error as Error }, + ); + } + + const gauge = rewards.rewardGauges[REWARD_GAUGE_KEY_BTC_DELEGATION]; + if (!gauge) { + console.log( + "[BbnQuery] BTC_STAKER gauge missing in response; returning empty", + ); + return [] as Array<{ denom: string; amount: number }>; + } + console.log("[BbnQuery] Gauge coins", gauge.coins); + console.log("[BbnQuery] Gauge withdrawnCoins", gauge.withdrawnCoins); + const totalByDenom = new Map(); + const withdrawnByDenom = new Map(); + + for (const coin of gauge.coins ?? []) { + const prev = totalByDenom.get(coin.denom) ?? 0; + totalByDenom.set(coin.denom, prev + Number(coin.amount)); + } + for (const coin of gauge.withdrawnCoins ?? []) { + const prev = withdrawnByDenom.get(coin.denom) ?? 0; + withdrawnByDenom.set(coin.denom, prev + Number(coin.amount)); + } + console.log( + "[BbnQuery] totalByDenom", + Array.from(totalByDenom.entries()), + ); + console.log( + "[BbnQuery] withdrawnByDenom", + Array.from(withdrawnByDenom.entries()), + ); + const results: Array<{ denom: string; amount: number }> = []; + for (const [denom, total] of totalByDenom.entries()) { + const withdrawn = withdrawnByDenom.get(denom) ?? 0; + const net = Math.max(0, total - withdrawn); + if (net > 0) { + results.push({ denom, amount: net }); + } + } + console.log("[BbnQuery] Net per-denom rewards", results); + return results; + }, + enabled: Boolean( + queryClient && + connected && + bech32Address && + !isGeoBlocked && + !isHealthcheckLoading, + ), + staleTime: ONE_MINUTE, + refetchInterval: ONE_MINUTE, + }); + /** * Gets the balance of the user's account. * @returns {Promise} - The balance of the user's account. @@ -172,6 +266,7 @@ export const useBbnQuery = () => { return { rewardsQuery, + rewardCoinsQuery, balanceQuery, btcTipQuery, babyTipQuery,