Skip to content

Commit 55caba8

Browse files
feat: expansion - verified UI (#180)
* feat(packages): expansion verified ui * chore(packages): format simple-staking * fix(packages): fp not showing in phase 2 delegation (#178) --------- Co-authored-by: Jeremy <168515712+jeremy-babylonlabs@users.noreply.github.com>
1 parent 7d948ce commit 55caba8

10 files changed

Lines changed: 189 additions & 213 deletions

File tree

services/simple-staking/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
}
1616
}
1717
}
18-
}
18+
}

services/simple-staking/src/ui/common/components/ActivityCard/ActivityCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ActivityCardActionButton {
2626
size?: "small" | "medium" | "large";
2727
className?: string;
2828
fullWidth?: boolean;
29+
disabled?: boolean;
2930
}
3031

3132
export interface ActivityCardData {

services/simple-staking/src/ui/common/components/ActivityCard/components/ActivityCardActionSection.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function ActivityCardActionSection({
1818
size={action.size || "small"}
1919
className={`sm:bbn-btn-medium ${action.fullWidth ? "w-full" : ""} ${action.className || ""}`}
2020
onClick={action.onClick}
21+
disabled={action.disabled}
2122
>
2223
{action.label}
2324
</Button>

services/simple-staking/src/ui/common/components/ActivityCard/components/ActivityCardAmountSection.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function ActivityCardAmountSection({
3939
size={primaryAction.size || "small"}
4040
className={`sm:bbn-btn-medium ${primaryAction.className || ""}`}
4141
onClick={primaryAction.onClick}
42+
disabled={primaryAction.disabled}
4243
>
4344
{primaryAction.label}
4445
</Button>

services/simple-staking/src/ui/common/components/ActivityCard/utils/activityCardTransformers.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Status } from "@/ui/common/components/Delegations/DelegationList/components/Status";
22
import { Hash } from "@/ui/common/components/Hash/Hash";
33
import { getNetworkConfigBTC } from "@/ui/common/config/network/btc";
4+
import { EXPANSION_OPERATIONS } from "@/ui/common/constants";
45
import {
56
DelegationV2,
67
DelegationV2StakingState,
@@ -9,6 +10,7 @@ import {
910
import { FinalityProvider } from "@/ui/common/types/finalityProviders";
1011
import { satoshiToBtc } from "@/ui/common/utils/btc";
1112
import { maxDecimals } from "@/ui/common/utils/maxDecimals";
13+
import { getExpansionType } from "@/ui/common/utils/stakingExpansionUtils";
1214
import { durationTillNow } from "@/ui/common/utils/time";
1315

1416
import { createBsnFpGroupedDetails } from "../../../utils/bsnFpGroupingUtils";
@@ -128,3 +130,69 @@ export function transformDelegationToActivityCard(
128130
hideExpansionCompletely: options.hideExpansionCompletely,
129131
};
130132
}
133+
134+
/**
135+
* Transforms a delegation into ActivityCard data specifically for verified expansions
136+
* Shows "Verified" status and includes expansion type information
137+
*/
138+
export function transformDelegationToVerifiedExpansionCard(
139+
delegation: DelegationV2,
140+
originalDelegation: DelegationV2,
141+
finalityProviderMap: Map<string, FinalityProvider>,
142+
): ActivityCardData {
143+
// Determine expansion type
144+
const operationType = getExpansionType(delegation, originalDelegation);
145+
146+
const details: ActivityCardDetailItem[] = [
147+
{
148+
label: "Status",
149+
value: "Verified",
150+
},
151+
{
152+
label: "Inception",
153+
value: delegation.bbnInceptionTime
154+
? durationTillNow(delegation.bbnInceptionTime, Date.now(), false)
155+
: "N/A",
156+
},
157+
{
158+
label: "Tx Hash",
159+
value: (
160+
<Hash
161+
value={delegation.stakingTxHashHex}
162+
address
163+
small
164+
noFade
165+
size="caption"
166+
/>
167+
),
168+
},
169+
{
170+
label: "Expansion Type",
171+
value:
172+
operationType === EXPANSION_OPERATIONS.RENEW_TIMELOCK
173+
? "Timelock Renewal"
174+
: "Added BSN/FP",
175+
},
176+
];
177+
178+
// Create grouped details for BSN/FP pairs with expansion support
179+
const groupedDetails = createBsnFpGroupedDetails(
180+
delegation.finalityProviderBtcPksHex,
181+
finalityProviderMap,
182+
{
183+
originalFinalityProviderBtcPksHex:
184+
originalDelegation.finalityProviderBtcPksHex,
185+
},
186+
);
187+
188+
const formattedAmount = `${maxDecimals(satoshiToBtc(delegation.stakingAmount), 8)} ${coinName}`;
189+
190+
return {
191+
formattedAmount,
192+
icon: icon,
193+
iconAlt: "bitcoin",
194+
details,
195+
groupedDetails: groupedDetails.length > 0 ? groupedDetails : undefined,
196+
hideExpansionCompletely: true, // Hide expansion section in verified modal
197+
};
198+
}

services/simple-staking/src/ui/common/components/ExpansionHistory/ExpansionHistoryModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function ExpansionHistoryModal({
4343
onClose={onClose}
4444
className="text-accent-primary"
4545
/>
46-
<DialogBody className="flex max-h-[70vh] flex-col gap-4 overflow-y-auto pb-4 pt-4 text-accent-primary">
46+
<DialogBody className="no-scrollbar flex max-h-[70vh] flex-col gap-4 overflow-y-auto pb-4 pt-4 text-accent-primary">
4747
<div className="flex flex-col gap-2">
4848
{!hasExpansionHistory ? (
4949
<div className="py-8 text-center">

services/simple-staking/src/ui/common/components/StakingExpansion/VerifiedStakeExpansionModal.tsx

Lines changed: 44 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,14 @@ import {
66
Text,
77
} from "@babylonlabs-io/core-ui";
88
import { useMemo, useState } from "react";
9-
import { BiSolidBadgeCheck } from "react-icons/bi";
109

10+
import { ActivityCard } from "@/ui/common/components/ActivityCard/ActivityCard";
11+
import { transformDelegationToVerifiedExpansionCard } from "@/ui/common/components/ActivityCard/utils/activityCardTransformers";
1112
import { ResponsiveDialog } from "@/ui/common/components/Modals/ResponsiveDialog";
12-
import { getNetworkConfigBBN } from "@/ui/common/config/network/bbn";
13-
import { getNetworkConfigBTC } from "@/ui/common/config/network/btc";
14-
import { EXPANSION_OPERATIONS } from "@/ui/common/constants";
1513
import { useVerifiedStakingExpansionService } from "@/ui/common/hooks/services/useVerifiedStakingExpansionService";
1614
import { useDelegationV2State } from "@/ui/common/state/DelegationV2State";
17-
import { useFinalityProviderBsnState } from "@/ui/common/state/FinalityProviderBsnState";
1815
import { useFinalityProviderState } from "@/ui/common/state/FinalityProviderState";
1916
import { DelegationV2 } from "@/ui/common/types/delegationsV2";
20-
import { satoshiToBtc } from "@/ui/common/utils/btc";
21-
import { maxDecimals } from "@/ui/common/utils/maxDecimals";
22-
import { trim } from "@/ui/common/utils/trim";
23-
24-
const { chainId: BBN_CHAIN_ID } = getNetworkConfigBBN();
25-
const { coinSymbol } = getNetworkConfigBTC();
26-
27-
// Helper function to determine expansion operation type
28-
const getExpansionType = (
29-
expansion: DelegationV2,
30-
original: DelegationV2 | undefined,
31-
) => {
32-
if (!original) {
33-
// If we can't find original, assume it's adding BSN/FP based on having previous tx
34-
return EXPANSION_OPERATIONS.ADD_BSN_FP;
35-
}
36-
37-
const newFPs = expansion.finalityProviderBtcPksHex.filter(
38-
(fp) => !original.finalityProviderBtcPksHex.includes(fp),
39-
);
40-
const fpsChanged = newFPs.length > 0;
41-
42-
if (fpsChanged) {
43-
return EXPANSION_OPERATIONS.ADD_BSN_FP; // Adding BSN/FP (always includes timelock renewal)
44-
} else {
45-
return EXPANSION_OPERATIONS.RENEW_TIMELOCK; // Pure timelock renewal only
46-
}
47-
48-
// TODO: Future expansion types to consider:
49-
// - INCREASE_AMOUNT (when staking amount > original amount)
50-
// - Mixed operations may be possible in future versions
51-
};
5217

5318
interface VerifiedStakeExpansionModalProps {
5419
open: boolean;
@@ -68,173 +33,52 @@ function VerifiedExpansionItem({
6833
processing,
6934
}: VerifiedExpansionItemProps) {
7035
const { findDelegationByTxHash } = useDelegationV2State();
71-
const { getRegisteredFinalityProvider } = useFinalityProviderState();
72-
const { bsnList } = useFinalityProviderBsnState();
73-
74-
// Parse BSN and FP information with proper expansion type detection
75-
const { bsnFpPairs, newCount, operationType, originalDelegation } =
76-
useMemo(() => {
77-
const pairs: Array<{ bsnName: string; fpName: string; isNew: boolean }> =
78-
[];
79-
80-
// Find the original delegation if this is an expansion
81-
const original = delegation.previousStakingTxHashHex
82-
? findDelegationByTxHash(delegation.previousStakingTxHashHex)
83-
: undefined;
84-
85-
// Determine the operation type
86-
const opType = getExpansionType(delegation, original);
87-
88-
// Create BSN/FP pairs with correct "new" detection
89-
delegation.finalityProviderBtcPksHex.forEach((fpPkHex) => {
90-
const provider = getRegisteredFinalityProvider(fpPkHex);
91-
const bsnId = provider?.bsnId || BBN_CHAIN_ID;
92-
const bsn = bsnList.find((b) => b.id === bsnId);
93-
94-
// Determine if this FP is new by checking if it exists in original delegation
95-
const isNewFP = original
96-
? !original.finalityProviderBtcPksHex.includes(fpPkHex)
97-
: false; // If no original delegation found, don't mark as new
98-
99-
pairs.push({
100-
bsnName: bsn?.name || "Babylon Genesis",
101-
fpName: provider?.description?.moniker || trim(fpPkHex, 8),
102-
isNew: isNewFP,
103-
});
104-
});
36+
const { finalityProviderMap } = useFinalityProviderState();
37+
38+
// Transform delegation to activity card data
39+
const activityCardData = useMemo(() => {
40+
// Find the original delegation - this should always exist for verified expansions
41+
const originalDelegation = delegation.previousStakingTxHashHex
42+
? findDelegationByTxHash(delegation.previousStakingTxHashHex)
43+
: undefined;
44+
45+
if (!originalDelegation) {
46+
console.error(
47+
"Original delegation not found for verified expansion:",
48+
delegation.stakingTxHashHex,
49+
"previousTxHash:",
50+
delegation.previousStakingTxHashHex,
51+
);
52+
// This should not happen for verified expansions, but return a fallback
53+
throw new Error(
54+
`Invalid verified expansion: original delegation not found for ${delegation.stakingTxHashHex}`,
55+
);
56+
}
10557

106-
const newPairsCount = pairs.filter((p) => p.isNew).length;
107-
return {
108-
bsnFpPairs: pairs,
109-
newCount: newPairsCount,
110-
operationType: opType,
111-
originalDelegation: original,
112-
};
113-
}, [
58+
return transformDelegationToVerifiedExpansionCard(
11459
delegation,
115-
findDelegationByTxHash,
116-
getRegisteredFinalityProvider,
117-
bsnList,
118-
]);
119-
120-
const stakingAmount = maxDecimals(satoshiToBtc(delegation.stakingAmount), 8);
60+
originalDelegation,
61+
finalityProviderMap,
62+
);
63+
}, [delegation, findDelegationByTxHash, finalityProviderMap]);
64+
65+
// Create the activity card data with primary action
66+
const activityCardDataWithAction = {
67+
...activityCardData,
68+
primaryAction: {
69+
label: "Expand",
70+
onClick: onExpand,
71+
variant: "contained" as const,
72+
size: "small" as const,
73+
disabled: processing,
74+
},
75+
};
12176

12277
return (
123-
<div className="space-y-3 rounded-lg border border-secondary-strokeLight p-4">
124-
<div className="flex items-start justify-between">
125-
<div className="flex-1 space-y-2">
126-
<div className="flex items-center gap-2">
127-
<BiSolidBadgeCheck className="text-xl text-primary-light" />
128-
<Text variant="body1" className="font-medium text-accent-primary">
129-
{operationType === EXPANSION_OPERATIONS.RENEW_TIMELOCK
130-
? "Timelock Renewal"
131-
: operationType === EXPANSION_OPERATIONS.ADD_BSN_FP
132-
? "BSN/FP Expansion"
133-
: "Verified Expansion"}
134-
</Text>
135-
</div>
136-
137-
<div className="space-y-1">
138-
<div className="flex items-center gap-2">
139-
<Text variant="body2" className="text-accent-secondary">
140-
Amount:
141-
</Text>
142-
<Text variant="body2" className="text-accent-primary">
143-
{stakingAmount} {coinSymbol}
144-
</Text>
145-
</div>
146-
147-
<div className="flex items-center gap-2">
148-
<Text variant="body2" className="text-accent-secondary">
149-
Transaction:
150-
</Text>
151-
<Text variant="body2" className="font-mono text-accent-primary">
152-
{trim(delegation.stakingTxHashHex, 10)}
153-
</Text>
154-
</div>
155-
156-
{/* Show different information based on operation type */}
157-
{operationType === EXPANSION_OPERATIONS.RENEW_TIMELOCK &&
158-
originalDelegation && (
159-
<div className="flex items-center gap-2">
160-
<Text variant="body2" className="text-accent-secondary">
161-
Timelock:
162-
</Text>
163-
<Text variant="body2" className="text-accent-primary">
164-
{originalDelegation.stakingTimelock.toLocaleString()} blocks
165-
{delegation.stakingTimelock.toLocaleString()} blocks
166-
</Text>
167-
</div>
168-
)}
169-
170-
{operationType === EXPANSION_OPERATIONS.ADD_BSN_FP && (
171-
<>
172-
{newCount > 0 && (
173-
<div className="flex items-center gap-2">
174-
<Text variant="body2" className="text-accent-secondary">
175-
New BSN/FP pairs:
176-
</Text>
177-
<Text variant="body2" className="text-accent-primary">
178-
{newCount}
179-
</Text>
180-
</div>
181-
)}
182-
{originalDelegation &&
183-
originalDelegation.stakingTimelock !==
184-
delegation.stakingTimelock && (
185-
<div className="flex items-center gap-2">
186-
<Text variant="body2" className="text-accent-secondary">
187-
Timelock renewed to:
188-
</Text>
189-
<Text variant="body2" className="text-accent-primary">
190-
{delegation.stakingTimelock.toLocaleString()} blocks
191-
</Text>
192-
</div>
193-
)}
194-
</>
195-
)}
196-
</div>
197-
198-
{/* Show BSN/FP pairs - different display for different operation types */}
199-
<div className="mt-3 space-y-1">
200-
<Text variant="body2" className="mb-1 text-accent-secondary">
201-
BSN / Finality Provider pairs:
202-
</Text>
203-
<div className="space-y-1">
204-
{bsnFpPairs.map((pair, index) => (
205-
<div key={index} className="flex items-center gap-2 text-sm">
206-
<Text
207-
variant="body2"
208-
className={
209-
pair.isNew ? "text-primary-light" : "text-accent-primary"
210-
}
211-
>
212-
{pair.bsnName} / {pair.fpName}
213-
</Text>
214-
{/* Only show NEW labels for ADD_BSN_FP operations and actually new pairs */}
215-
{operationType === EXPANSION_OPERATIONS.ADD_BSN_FP &&
216-
pair.isNew && (
217-
<span className="rounded bg-primary-light/10 px-2 py-0.5 text-xs text-primary-light">
218-
NEW
219-
</span>
220-
)}
221-
</div>
222-
))}
223-
</div>
224-
</div>
225-
</div>
226-
227-
<Button
228-
variant="contained"
229-
size="small"
230-
onClick={onExpand}
231-
disabled={processing}
232-
className="ml-4"
233-
>
234-
Expand
235-
</Button>
236-
</div>
237-
</div>
78+
<ActivityCard
79+
data={activityCardDataWithAction}
80+
className="border border-secondary-strokeLight"
81+
/>
23882
);
23983
}
24084

0 commit comments

Comments
 (0)