Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.
Closed
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
365 changes: 361 additions & 4 deletions src/ui/baby/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,366 @@
import Layout from "@/ui/legacy/layout";
import { useState } from "react";

import { useCosmosWallet } from "@/ui/common/context/wallet/CosmosWalletProvider";
import { useBbnQuery } from "@/ui/legacy/hooks/client/rpc/queries/useBbnQuery";
import { useEpochingService } from "@/ui/legacy/hooks/services/useEpochingService";
import { babyToUbbn, ubbnToBaby } from "@/ui/legacy/utils/bbn";

export default function BabyStaking() {
const cosmosWallet = useCosmosWallet();
const { bech32Address, connected } = cosmosWallet;
console.log({ cosmosWallet });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to remove?

const { stake, unstake, claimRewards } = useEpochingService();
const { delegationsQuery, delegationRewardsQuery, validatorsQuery } =
useBbnQuery();

const [validatorAddress, setValidatorAddress] = useState("");
const [stakeAmount, setStakeAmount] = useState("");
const [unstakeAmount, setUnstakeAmount] = useState("");
const [loading, setLoading] = useState(false);

const delegations = delegationsQuery.data || [];
const rewards = delegationRewardsQuery.data || [];
const validators = validatorsQuery.data || [];

// Helper function to get rewards for a specific validator
const getRewardsForValidator = (validatorAddress: string): number => {
const validatorReward = rewards.find(
(reward) => reward.validatorAddress === validatorAddress,
);
if (!validatorReward?.reward) return 0;

return validatorReward.reward.reduce((total, coin) => {
if (coin.denom === "ubbn") {
return total + ubbnToBaby(parseFloat(coin.amount));
}
return total;
}, 0);
};

const totalStaked = delegations.reduce((total, delegation) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add anything does not fit into the MVP. The more code we have, the more issues/bug we may discover which delay the delivery.
Keep it simply and lean is the best option for now

if (delegation.balance?.denom === "ubbn") {
return total + ubbnToBaby(parseFloat(delegation.balance.amount));
}
return total;
}, 0);

const totalRewards = rewards.reduce((total, reward) => {
return (
total +
(reward.reward || []).reduce((acc, coin) => {
if (coin.denom === "ubbn") {
return acc + ubbnToBaby(parseFloat(coin.amount));
}
return acc;
}, 0)
);
}, 0);
Comment thread
jrwbabylonlab marked this conversation as resolved.

const handleStake = async () => {
if (!validatorAddress || !stakeAmount) return;

const tbabyAmount = parseFloat(stakeAmount);
if (isNaN(tbabyAmount) || tbabyAmount <= 0) {
alert("Please enter a valid amount");
return;
}

setLoading(true);
try {
const ubbnAmount = babyToUbbn(tbabyAmount);
await stake(bech32Address, validatorAddress, {
denom: "ubbn",
amount: ubbnAmount.toString(),
});
alert(`Successfully staked ${tbabyAmount} tBABY!`);
await delegationsQuery.refetch();
await delegationRewardsQuery.refetch();
setStakeAmount("");
} catch (error: any) {
alert(`Staking failed: ${error.message || "Unknown error"}`);
} finally {
setLoading(false);
}
};

const handleUnstake = async () => {
if (!validatorAddress || !unstakeAmount) return;

const tbabyAmount = parseFloat(unstakeAmount);
if (isNaN(tbabyAmount) || tbabyAmount <= 0) {
alert("Please enter a valid amount");
return;
}

setLoading(true);
try {
const ubbnAmount = babyToUbbn(tbabyAmount);
await unstake(bech32Address, validatorAddress, {
denom: "ubbn",
amount: ubbnAmount.toString(),
});
alert(`Successfully unstaked ${tbabyAmount} tBABY!`);
await delegationsQuery.refetch();
await delegationRewardsQuery.refetch();
setUnstakeAmount("");
} catch (error: any) {
alert(`Unstaking failed: ${error.message || "Unknown error"}`);
} finally {
setLoading(false);
}
};

const handleClaimRewards = async () => {
if (!validatorAddress) return;

setLoading(true);
try {
await claimRewards(bech32Address, validatorAddress);
await delegationRewardsQuery.refetch();
alert("Rewards claimed successfully!");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to have alert for successful event?

} catch (error: any) {
alert(`Failed to claim rewards: ${error.message || "Unknown error"}`);
} finally {
setLoading(false);
}
};

if (!connected) {
return (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">Connect Your Wallet</h2>
<p className="text-gray-600">
Please connect your Babylon wallet to use staking features.
</p>
</div>
</div>
);
}

return (
<Layout>
<div className="h-[500px]">Baby staking</div>
</Layout>
<div className="container mx-auto p-6 max-w-4xl">
<h1 className="text-3xl font-bold mb-8 text-center">Baby Staking</h1>

<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Wallet Info</h2>
<p className="text-sm text-gray-600 mb-2">Address: {bech32Address}</p>
<div className="mt-4 grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p className="text-sm font-medium">Active Delegations</p>
<p className="text-lg font-bold">{delegations.length}</p>
</div>
<div>
<p className="text-sm font-medium">Total Staked</p>
<p className="text-lg font-bold">
{totalStaked.toLocaleString()} tBABY
</p>
</div>
<div>
<p className="text-sm font-medium">Total Rewards</p>
<p className="text-lg font-bold text-green-600">
{totalRewards.toLocaleString()} tBABY
</p>
</div>
<div>
<p className="text-sm font-medium">Available Validators</p>
<p className="text-lg font-bold">{validators.length}</p>
</div>
</div>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we have a dedicated tab for rewards? In that case we don't need this stats tab for MVP.
User can refer to their wallet for balance and rewards tab for the rewards amount


{delegations.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">My Delegations</h2>
<div className="space-y-2">
{delegations.map((delegation, index) => {
const validatorAddress =
delegation.delegation?.validatorAddress || "";
const stakedAmount = ubbnToBaby(
parseFloat(delegation.balance?.amount || "0"),
);
const rewardsAmount = getRewardsForValidator(validatorAddress);

@jrwbabylonlab jrwbabylonlab Jul 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, not fully understanding what we showing here. are you plan to show rewards per delegation? i could not find this in the design file.
We should have a single button to claim all rewards instead


return (
<div
key={index}
className="flex justify-between items-center p-3 border rounded"
>
<div>
<p className="font-medium">
{validators.find(
(v) => v.operatorAddress === validatorAddress,
)?.description?.moniker || validatorAddress}
</p>
<p className="text-sm text-gray-600">
Staked: {stakedAmount.toLocaleString()} tBABY
</p>
{rewardsAmount > 0 && (
<p className="text-sm text-green-600">
Rewards: {rewardsAmount.toLocaleString()} tBABY
</p>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => {
setValidatorAddress(validatorAddress);
setUnstakeAmount(stakedAmount.toString());
}}
className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600"
>
Unstake All
</button>
{rewardsAmount > 0 && (
<button
onClick={async () => {
setValidatorAddress(validatorAddress);
await handleClaimRewards();
}}
disabled={loading}
className="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600 disabled:bg-gray-300"
>
{loading ? "Claiming..." : "Claim"}
</button>
)}
</div>
</div>
);
})}
</div>
</div>
)}

<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Stake Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Stake</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Validator
</label>
<select
value={validatorAddress}
onChange={(e) => setValidatorAddress(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a validator</option>
{validators.map((validator) => (
<option
key={validator.operatorAddress}
value={validator.operatorAddress}
>
{validator.description?.moniker ||
validator.operatorAddress}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Amount (tBABY)
</label>
<input
type="text"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter tBABY amount to stake (e.g., 10)"
/>
</div>
<button
onClick={handleStake}
disabled={loading || !validatorAddress || !stakeAmount}
className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{loading ? "Staking..." : "Stake tBABY"}
</button>
</div>
</div>

{/* Unstake Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Unstake</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Validator
</label>
<select
value={validatorAddress}
onChange={(e) => setValidatorAddress(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
>
<option value="">Select a validator</option>
{validators.map((validator) => (
<option
key={validator.operatorAddress}
value={validator.operatorAddress}
>
{validator.description?.moniker ||
validator.operatorAddress}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Amount (tBABY)
</label>
<input
type="text"
value={unstakeAmount}
onChange={(e) => setUnstakeAmount(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
placeholder="Enter tBABY amount to unstake (e.g., 5)"
/>
</div>
<button
onClick={handleUnstake}
disabled={loading || !validatorAddress || !unstakeAmount}
className="w-full bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{loading ? "Unstaking..." : "Unstake tBABY"}
</button>
</div>
</div>

{/* Rewards Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Rewards</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Validator
</label>
<select
value={validatorAddress}
onChange={(e) => setValidatorAddress(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value="">Select a validator</option>
{validators.map((validator) => (
<option
key={validator.operatorAddress}
value={validator.operatorAddress}
>
{validator.description?.moniker ||
validator.operatorAddress}
</option>
))}
</select>
</div>
<button
onClick={handleClaimRewards}
disabled={loading || !validatorAddress}
className="w-full bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{loading ? "Claiming..." : "Claim Rewards"}
</button>
</div>
</div>
</div>
</div>
);
}
Loading