Skip to content

feat: new finality provider table design #1154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8968bfb
feat: update existing services to adopt multi-staking compatible btc-…
jrwbabylonlab Jun 17, 2025
193ba2e
Readd chain selection modal
jonybur Jun 17, 2025
73ab200
Merge branch 'adopt-multi-staking-compatible-btc-ts' of github.com:ba…
jonybur Jun 17, 2025
749c5f9
feat: add bsn list
jeremy-babylonlabs Jun 16, 2025
9b9b8b1
fix: resolve comments
jeremy-babylonlabs Jun 17, 2025
7c14ebb
fix: using finalityProviderBsnState
jeremy-babylonlabs Jun 18, 2025
001cfa4
fix: provider Ids
jeremy-babylonlabs Jun 18, 2025
b7a7b25
fix: remove unnecessary wrapper
jeremy-babylonlabs Jun 18, 2025
8981bf4
Merge branch 'feat/1127-bsn-list' of github.com:babylonlabs-io/simple…
jonybur Jun 18, 2025
41dbbe7
Implement endpoints
jonybur Jun 18, 2025
f4e34a7
Add validations
jonybur Jun 19, 2025
854390b
feat: add bsn list
jeremy-babylonlabs Jun 16, 2025
c5d5816
fix: resolve comments
jeremy-babylonlabs Jun 17, 2025
62ca03c
fix: using finalityProviderBsnState
jeremy-babylonlabs Jun 18, 2025
299b52a
fix: provider Ids
jeremy-babylonlabs Jun 18, 2025
e03373b
fix: remove unnecessary wrapper
jeremy-babylonlabs Jun 18, 2025
a4e4c65
feat: add max bsn fp providers
jeremy-babylonlabs Jun 19, 2025
7ac103b
Implement endpoints
jonybur Jun 18, 2025
7c5133b
Add validations
jonybur Jun 19, 2025
70e5a7e
Merge branch 'jb-implement-chain-selection' of github.com:babylonlabs…
jonybur Jun 19, 2025
1333fc8
Add filter
jonybur Jun 19, 2025
ea292b6
Merge branch 'main' of github.com:babylonlabs-io/simple-staking into …
jonybur Jun 19, 2025
eeb4436
Remove unused code
jonybur Jun 19, 2025
08d7cb5
Fixes
jonybur Jun 19, 2025
9fdf410
Duplicate components, link to bsn state
jonybur Jun 19, 2025
26361c3
Revert changes
jonybur Jun 19, 2025
ef2e2ca
Remove unused argument
jonybur Jun 19, 2025
3ada6ae
Implement FP tables new design
jonybur Jun 20, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export function BsnFinalityProviderField({
disabled,
});

const handleAdd = () => {
// TODO: Implement provider selection logic
const handleAdd = (providerPk: string) => {
// Add selected provider ID to list
onChange([...selectedProviderIds, providerPk]);
onClose();
};

Expand Down Expand Up @@ -62,7 +63,12 @@ export function BsnFinalityProviderField({
/>
)}
</div>
<BsnModal open={open} onAdd={handleAdd} onClose={onClose} />
<BsnModal
open={open}
onAdd={handleAdd}
onClose={onClose}
selectedProviderIds={selectedProviderIds}
/>
</SubSection>
);
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,87 @@
import { DialogBody, DialogHeader } from "@babylonlabs-io/core-ui";
import { useEffect, useMemo, useState } from "react";

import { ResponsiveDialog } from "@/ui/components/Modals/ResponsiveDialog";
import { ChainSelectionModal } from "@/ui/components/Multistaking/ChainSelectionModal/ChainSelectionModal";
import { FinalityProviderModal } from "@/ui/components/Multistaking/FinalityProviderField/FinalityProviderModal";
import {
FinalityProviderBsnState,
useFinalityProviderBsnState,
} from "@/ui/state/FinalityProviderBsnState";

interface Props {
open: boolean;
defaultFinalityProvider?: string;
onAdd: () => void;
onAdd: (selectedProviderPk: string) => void;
onClose: () => void;
selectedProviderIds: string[];
}

export function BsnModal({
open,
// defaultFinalityProvider,
// onAdd,
onClose,
}: Props) {
enum BsnModalPage {
CHAIN = "CHAIN",
FP = "FP",
}

export function BsnModal({ open, onAdd, onClose, selectedProviderIds }: Props) {
const [selectedChainId, setSelectedChainId] = useState<string | null>(null);
const [page, setPage] = useState<BsnModalPage>(BsnModalPage.CHAIN);

const { getRegisteredFinalityProvider } = useFinalityProviderBsnState();

const hasBabylonProvider = useMemo(
() =>
selectedProviderIds.some((pk) => {
const fp = getRegisteredFinalityProvider(pk);
return !fp?.bsnId;
}),
[selectedProviderIds, getRegisteredFinalityProvider],
);

const disabledChainIds = useMemo(() => {
const set = new Set<string>();
selectedProviderIds.forEach((pk) => {
const fp = getRegisteredFinalityProvider(pk);
set.add(fp?.bsnId || "");
});
return Array.from(set);
}, [selectedProviderIds, getRegisteredFinalityProvider]);

useEffect(() => {
if (!open) {
return;
}
setPage(BsnModalPage.CHAIN);
setSelectedChainId(null);
}, [open]);

const handleChainNext = (chainId: string) => {
setSelectedChainId(chainId);
setPage(BsnModalPage.FP);
};

const handleProviderAdd = (providerPk: string) => {
onAdd(providerPk);
};

return (
<ResponsiveDialog open={open} onClose={onClose} className="w-[52rem]">
<DialogHeader
title="Select BSN and Finality Provider"
onClose={onClose}
className="text-accent-primary"
/>
<DialogBody className="flex flex-col mb-4 mt-4 text-accent-primary">
{/* TODO: Implement BSN selection functionality */}
</DialogBody>
{page === BsnModalPage.CHAIN && (
<ChainSelectionModal
onNext={handleChainNext}
onClose={onClose}
disableNonBabylon={!hasBabylonProvider}
disabledChainIds={disabledChainIds}
/>
)}
{page === BsnModalPage.FP && selectedChainId !== null && (
<FinalityProviderBsnState bsnId={selectedChainId}>
<FinalityProviderModal
open={true}
defaultFinalityProvider=""
onClose={onClose}
onAdd={handleProviderAdd}
onBack={() => setPage(BsnModalPage.CHAIN)}
/>
</FinalityProviderBsnState>
)}
</ResponsiveDialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
DialogHeader,
Text,
} from "@babylonlabs-io/core-ui";
import { useQuery } from "@tanstack/react-query";
import { PropsWithChildren, useState } from "react";
import { MdOutlineInfo } from "react-icons/md";
import { twMerge } from "tailwind-merge";

import { getBSNs } from "@/ui/api/getBsn";
import { chainLogos } from "@/ui/constants";

const SubSection = ({
Expand Down Expand Up @@ -80,12 +82,21 @@ const ChainButton = ({
export const ChainSelectionModal = ({
onNext,
onClose,
disableNonBabylon = false,
disabledChainIds = [],
}: {
onNext: (selectedChain: string) => void;
onClose: () => void;
disableNonBabylon?: boolean;
disabledChainIds?: string[];
}) => {
const [selected, setSelected] = useState<string | null>(null);

const { data: bsns, isLoading } = useQuery({
queryKey: ["API_BSN_LIST"],
queryFn: getBSNs,
});

return (
<>
<DialogHeader
Expand All @@ -101,47 +112,48 @@ export const ChainSelectionModal = ({
rewards.
</div>
<div className="overflow-x-auto flex flex-col gap-2 mt-10">
<ChainButton
logo={chainLogos.babylon}
title="Babylon"
selected={selected === "babylon"}
onClick={() => setSelected("babylon")}
/>
<ChainButton
logo={chainLogos.cosmos}
title="Cosmos"
disabled
onClick={() => setSelected("cosmos")}
/>
<ChainButton
logo={chainLogos.ethereum}
title="Ethereum"
disabled
onClick={() => setSelected("ethereum")}
/>
<ChainButton
logo={chainLogos.sui}
title="Sui"
disabled
onClick={() => setSelected("sui")}
/>
{isLoading && <div>Loading...</div>}
{bsns?.map((bsn) => {
const logo =
bsn.id === ""
? chainLogos.babylon
: (chainLogos as Record<string, string>)[bsn.id] ||
chainLogos.placeholder;

const isDisabled =
(disableNonBabylon && bsn.id !== "") ||
disabledChainIds.includes(bsn.id);

return (
<ChainButton
key={bsn.id}
logo={logo}
title={bsn.name}
selected={selected === bsn.id}
disabled={isDisabled}
onClick={() => !isDisabled && setSelected(bsn.id)}
/>
);
})}
</div>
<SubSection className="text-base text-[#387085] gap-3 flex-row mt-4">
<div>
<MdOutlineInfo size={22} />
</div>
<div>
Babylon must be the first BSN you add before selecting others. Once
added, you can choose additional BSNs to multistake.
</div>
</SubSection>
{disableNonBabylon && (
<SubSection className="text-base text-[#387085] gap-3 flex-row mt-4">
<div>
<MdOutlineInfo size={22} />
</div>
<div>
Babylon must be the first BSN you add before selecting others.
Once added, you can choose additional BSNs to multistake.
</div>
</SubSection>
)}
</DialogBody>

<DialogFooter className="flex justify-end">
<Button
variant="contained"
onClick={() => selected && onNext(selected)}
disabled={!selected}
onClick={() => selected !== null && onNext(selected)}
disabled={selected === null}
>
Next
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FinalityProviderFilter } from "@/ui/components/Staking/FinalityProviders/FinalityProviderFilter";
import { FinalityProviderSearch } from "@/ui/components/Staking/FinalityProviders/FinalityProviderSearch";
import { FinalityProviderTable } from "@/ui/components/Staking/FinalityProviders/FinalityProviderTable";
import { FinalityProviderFilter } from "@/ui/components/Multistaking/FinalityProviders/FinalityProviderFilter";
import { FinalityProviderSearch } from "@/ui/components/Multistaking/FinalityProviders/FinalityProviderSearch";
import { FinalityProviderTable } from "@/ui/components/Multistaking/FinalityProviders/FinalityProviderTable";

interface Props {
selectedFP: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Select } from "@babylonlabs-io/core-ui";

import { useFinalityProviderBsnState } from "@/ui/state/FinalityProviderBsnState";

const options = [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
];

export const FinalityProviderFilter = () => {
const { filter, handleFilter } = useFinalityProviderBsnState();

return (
<Select
options={options}
onSelect={(value) => handleFilter("status", value.toString())}
placeholder="Select Status"
value={filter.search ? "" : filter.status}
disabled={Boolean(filter.search)}
renderSelectedOption={(option) => `Showing ${option.label}`}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Input } from "@babylonlabs-io/core-ui";
import { useCallback } from "react";
import { MdCancel } from "react-icons/md";
import { RiSearchLine } from "react-icons/ri";

import { useFinalityProviderBsnState } from "@/ui/state/FinalityProviderBsnState";

export const FinalityProviderSearch = () => {
const { filter, handleFilter } = useFinalityProviderBsnState();

const onSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
handleFilter("search", e.target.value);
},
[handleFilter],
);

const onClearSearch = useCallback(() => {
handleFilter("search", "");
}, [handleFilter]);

const searchSuffix = filter.search ? (
<button
onClick={onClearSearch}
className="opacity-60 hover:opacity-100 transition-opacity"
>
<MdCancel size={18} className="text-secondary-strokeDark" />
</button>
) : (
<span className="text-secondary-strokeDark">
<RiSearchLine size={20} />
</span>
);

return (
<Input
placeholder="Search by Name or Public Key"
suffix={searchSuffix}
value={filter.search}
onChange={onSearchChange}
/>
);
};
Loading