Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.
Open
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
3 changes: 2 additions & 1 deletion src/ui/baby/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function BabyLayoutContent() {
as="main"
className="flex flex-col gap-[3rem] pb-24 max-w-[760px] mx-auto flex-1"
>
<Tabs items={fallbackTabItems} defaultActiveTab="stake" />
<Tabs items={fallbackTabItems} defaultActiveTab="stake" keepMounted />
</Container>
);

Expand All @@ -132,6 +132,7 @@ function BabyLayoutContent() {
defaultActiveTab="stake"
activeTab={activeTab}
onTabChange={(tabId) => setActiveTab(tabId as TabId)}
keepMounted
/>
</Container>
</AuthGuard>
Expand Down
64 changes: 64 additions & 0 deletions src/ui/baby/widgets/StakingForm/BabyFormPersistence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useFormContext, useWatch } from "@babylonlabs-io/core-ui";
import { useEffect, useMemo, useRef } from "react";

import {
useFormPersistenceState,
type BabyStakeDraft,
} from "@/ui/common/state/FormPersistenceState";

export function BabyFormPersistence() {
const { setValue, control } = useFormContext<BabyStakeDraft>();
const { babyStakeDraft, setBabyStakeDraft } = useFormPersistenceState();
const hasHydratedRef = useRef(false);

const amount = useWatch({ control, name: "amount" });
const validatorAddresses = useWatch({ control, name: "validatorAddresses" });
const feeAmount = useWatch({ control, name: "feeAmount" });

// Hydrate once on mount
useEffect(() => {
if (hasHydratedRef.current) return;

if (babyStakeDraft) {
if (babyStakeDraft.amount !== undefined) {
setValue("amount", babyStakeDraft.amount, {
shouldValidate: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we have comments on these values so it's easier to understand why, besides Hydrate once on mount

shouldDirty: false,
shouldTouch: false,
});
}
if (babyStakeDraft.validatorAddresses !== undefined) {
setValue("validatorAddresses", babyStakeDraft.validatorAddresses, {
shouldValidate: true,
shouldDirty: false,
shouldTouch: false,
});
}
if (babyStakeDraft.feeAmount !== undefined) {
setValue("feeAmount", babyStakeDraft.feeAmount, {
shouldValidate: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could probably move these {shouldValidate: true, shouldDirty: false, shouldTouch: false} to a const and assign on each of these setValues. Would make the code a bit cleaner.

Actually, since we are at it, this is simply a matter of doing something like

["amount", "validatorAddress", "feeAmount"].forEach(value => {
 if (babyStakeDraft[value] !== undefined) setValue(value, /*object here*/)
}

shouldDirty: false,
shouldTouch: false,
});
}
}

hasHydratedRef.current = true;
}, [babyStakeDraft, setValue]);

const draft = useMemo(
() => ({
amount,
validatorAddresses,
feeAmount,
}),
[amount, validatorAddresses, feeAmount],
);

// Persist on change
useEffect(() => {
setBabyStakeDraft(draft);
}, [draft, setBabyStakeDraft]);

return null;
}
3 changes: 3 additions & 0 deletions src/ui/baby/widgets/StakingForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { SubmitButton } from "@/ui/baby/widgets/SubmitButton";
import { ValidatorField } from "@/ui/baby/widgets/ValidatorField";
import { FormAlert } from "@/ui/common/components/Multistaking/MultistakingForm/FormAlert";

import { BabyFormPersistence } from "./BabyFormPersistence";

interface FormFields {
amount: number;
validatorAddresses: string[];
Expand Down Expand Up @@ -44,6 +46,7 @@ export default function StakingForm({
className="flex flex-col gap-2 h-[500px]"
onSubmit={handlePreview}
>
<BabyFormPersistence />
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this a functional component? can't this be a hook instead? Since it's returning null that is

Copy link
Collaborator

Choose a reason for hiding this comment

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

agree

<AmountField balance={availableBalance} price={babyPrice} />
<ValidatorField />
<FeeField babyPrice={babyPrice} calculateFee={calculateFee} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useFormContext, useWatch } from "@babylonlabs-io/core-ui";
import { useEffect, useMemo, useRef } from "react";

import {
useFormPersistenceState,
type BtcStakeDraft,
} from "@/ui/common/state/FormPersistenceState";
import { useStakingState } from "@/ui/common/state/StakingState";

export function BtcFormPersistence() {
const { setValue, control } = useFormContext<BtcStakeDraft>();
const { btcStakeDraft, setBtcStakeDraft } = useFormPersistenceState();
const { stakingInfo } = useStakingState();
const hasHydratedRef = useRef(false);

const finalityProviders = useWatch({ control, name: "finalityProviders" });
const amount = useWatch({ control, name: "amount" });
const term = useWatch({ control, name: "term" });
const feeRate = useWatch({ control, name: "feeRate" });
const feeAmount = useWatch({ control, name: "feeAmount" });

// Hydrate once on mount
useEffect(() => {
if (hasHydratedRef.current) return;

if (btcStakeDraft) {
if (btcStakeDraft.finalityProviders) {
setValue("finalityProviders", btcStakeDraft.finalityProviders, {
shouldValidate: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

shouldDirty: false,
shouldTouch: false,
});
}
if (btcStakeDraft.amount !== undefined) {
setValue("amount", btcStakeDraft.amount, {
shouldValidate: true,
shouldDirty: false,
shouldTouch: false,
});
}
if (btcStakeDraft.term !== undefined) {
setValue("term", btcStakeDraft.term, {
shouldValidate: false,
shouldDirty: false,
shouldTouch: false,
});
}
if (btcStakeDraft.feeRate !== undefined) {
setValue("feeRate", btcStakeDraft.feeRate, {
shouldValidate: true,
shouldDirty: false,
shouldTouch: false,
});
}
if (btcStakeDraft.feeAmount !== undefined) {
setValue("feeAmount", btcStakeDraft.feeAmount, {
shouldValidate: true,
shouldDirty: false,
shouldTouch: false,
});
}
} else if (
stakingInfo?.defaultFeeRate !== undefined &&
(feeRate === undefined || feeRate === "")
Comment on lines +63 to +64
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

[nitpick] The condition feeRate === \"\" should use a more robust check. Consider using a utility function to check for empty/falsy values consistently across the codebase, or at minimum check for both \"\" and \"0\".

Suggested change
stakingInfo?.defaultFeeRate !== undefined &&
(feeRate === undefined || feeRate === "")
(feeRate === undefined || feeRate === "" || feeRate === "0")

Copilot uses AI. Check for mistakes.

) {
// Apply default only when no persisted draft and no current value
setValue("feeRate", stakingInfo.defaultFeeRate.toString(), {
shouldValidate: true,
shouldDirty: false,
shouldTouch: false,
});
}

hasHydratedRef.current = true;
}, [btcStakeDraft, stakingInfo?.defaultFeeRate, feeRate, setValue]);

const draft = useMemo(
() => ({
finalityProviders,
amount,
term,
feeRate,
feeAmount,
}),
[finalityProviders, amount, term, feeRate, feeAmount],
);

// Persist on change
useEffect(() => {
setBtcStakeDraft(draft);
}, [draft, setBtcStakeDraft]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useBTCWallet } from "@/ui/common/context/wallet/BTCWalletProvider";
import { useStakingState } from "@/ui/common/state/StakingState";

import { AmountSection } from "./AmountSection";
import { BtcFormPersistence } from "./BtcFormPersistence";
import { ConnectButton } from "./ConnectButton";
import { FinalityProvidersSection } from "./FinalityProvidersSection";
import { FormAlert } from "./FormAlert";
Expand All @@ -28,6 +29,8 @@ export function MultistakingFormContent() {
<HiddenField name="feeRate" defaultValue="0" />
<HiddenField name="feeAmount" defaultValue="0" />

<BtcFormPersistence />

<div className="flex flex-col gap-2">
<FinalityProvidersSection />
<AmountSection />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { getNetworkConfigBTC } from "@/ui/common/config/network/btc";
import { BBN_FEE_AMOUNT } from "@/ui/common/constants";
import { usePrice } from "@/ui/common/hooks/client/api/usePrices";
import { useStakingService } from "@/ui/common/hooks/services/useStakingService";
import { useStakingState } from "@/ui/common/state/StakingState";
import { satoshiToBtc } from "@/ui/common/utils/btc";
import { calculateTokenValueInCurrency } from "@/ui/common/utils/formatCurrency";
import { maxDecimals } from "@/ui/common/utils/maxDecimals";
Expand All @@ -30,17 +29,6 @@ export function StakingFeesSection() {
);

const { calculateFeeAmount } = useStakingService();
const { stakingInfo } = useStakingState();

useEffect(() => {
if (stakingInfo?.defaultFeeRate !== undefined) {
setValue("feeRate", stakingInfo.defaultFeeRate.toString(), {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
}
}, [stakingInfo?.defaultFeeRate, setValue]);

useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -162,6 +150,7 @@ export function StakingFeesSection() {
open={feeModalVisible}
onClose={() => setFeeModalVisible(false)}
onSubmit={handleFeeRateSubmit}
currentFeeRate={Number(feeRate || 0)}
/>
</>
);
Expand Down
34 changes: 33 additions & 1 deletion src/ui/common/components/Staking/FeeModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ interface FeeModalProps {
open?: boolean;
onSubmit?: (value: number) => void;
onClose?: () => void;
currentFeeRate?: number;
}

export function FeeModal({ open, onSubmit, onClose }: FeeModalProps) {
export function FeeModal({
open,
onSubmit,
onClose,
currentFeeRate,
}: FeeModalProps) {
const [selectedValue, setSelectedValue] = useState("");
const [customFee, setCustomFee] = useState("");
const customFeeRef = useRef<HTMLInputElement>(null);
Expand All @@ -45,6 +51,32 @@ export function FeeModal({ open, onSubmit, onClose }: FeeModalProps) {
}
}, [selectedValue]);

// Initialize selection based on current fee rate when opening
useEffect(() => {
if (!open || isLoading) return;

const fee = Number(currentFeeRate);
if (!fee || !Number.isFinite(fee)) {
Comment on lines +58 to +59
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

The condition !fee will be true for valid fee rate of 0, which should be allowed. Change to !Number.isFinite(fee) || fee <= 0 to properly handle zero values while rejecting invalid numbers.

Suggested change
const fee = Number(currentFeeRate);
if (!fee || !Number.isFinite(fee)) {
if (!Number.isFinite(fee) || fee < 0) {

Copilot uses AI. Check for mistakes.

setSelectedValue("");
setCustomFee("");
return;
}

if (fee === fastestFee) {
setSelectedValue("fast");
setCustomFee("");
} else if (fee === mediumFee) {
setSelectedValue("medium");
setCustomFee("");
} else if (fee === lowestFee) {
setSelectedValue("slow");
setCustomFee("");
} else {
setSelectedValue("custom");
setCustomFee(fee.toString());
}
}, [open, isLoading, currentFeeRate, fastestFee, mediumFee, lowestFee]);

const feeOptions = [
{
label: (
Expand Down
34 changes: 26 additions & 8 deletions src/ui/common/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface TabsProps {
activeTab?: string;
onTabChange?: (tabId: string) => void;
className?: string;
keepMounted?: boolean;
}

export const Tabs = ({
Expand All @@ -21,6 +22,7 @@ export const Tabs = ({
activeTab: controlledActiveTab,
onTabChange,
className,
keepMounted,
}: TabsProps) => {
const [internalActiveTab, setInternalActiveTab] = useState(
defaultActiveTab || items[0]?.id || "",
Expand Down Expand Up @@ -69,14 +71,30 @@ export const Tabs = ({
))}
</div>

<div
className="mt-6 min-h-[450px]"
role="tabpanel"
id={`panel-${activeTab}`}
aria-labelledby={`tab-${activeTab}`}
>
{activeContent}
</div>
{keepMounted ? (
<div className="mt-6 min-h-[450px]">
{items.map((item) => (
<div
key={item.id}
role="tabpanel"
id={`panel-${item.id}`}
aria-labelledby={`tab-${item.id}`}
className={twMerge(activeTab === item.id ? "" : "hidden")}
>
{item.content}
</div>
))}
</div>
) : (
<div
className="mt-6 min-h-[450px]"
role="tabpanel"
id={`panel-${activeTab}`}
aria-labelledby={`tab-${activeTab}`}
>
{activeContent}
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions src/ui/common/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const Home = () => {
defaultActiveTab="stake"
activeTab={activeTab}
onTabChange={setActiveTab}
keepMounted
/>
</Container>
);
Expand Down
Loading