From 905c237af1f80a10a3c442e9a6766af644e7a279 Mon Sep 17 00:00:00 2001 From: Robert Brada Date: Wed, 11 Feb 2026 17:00:33 +0100 Subject: [PATCH 1/4] feat: additional atp support --- .gitignore | 1 + atp-indexer/.env.example | 9 +- atp-indexer/bootstrap.sh | 9 ++ atp-indexer/ponder.config.ts | 43 ++++++- atp-indexer/ponder.schema.ts | 2 + .../src/api/handlers/atp/beneficiary.ts | 1 + atp-indexer/src/api/types/atp.types.ts | 1 + atp-indexer/src/config/index.ts | 4 + .../src/events/atp-factory/atp-created.ts | 14 ++- .../ATPDetailsDelegationItem.tsx | 12 +- .../ATPDetailsDirectStakeItem.tsx | 9 +- .../ATPDetailsModal/ATPDetailsModal.tsx | 12 ++ .../ATPDetailsModal/WithdrawalActions.tsx | 64 +++++++++- .../ATPStakingCard/ATPStakingCard.tsx | 31 ++++- .../MilestoneStatusBadge.tsx | 54 +++++++++ .../components/MilestoneStatusBadge/index.ts | 1 + .../src/hooks/atp/atpBaseTypes.ts | 1 + .../src/hooks/atp/useAtpHoldings.ts | 1 + .../src/hooks/atp/useMultipleAtpData.ts | 7 +- .../src/hooks/atpRegistry/index.ts | 1 + .../hooks/atpRegistry/useMilestoneStatus.ts | 111 ++++++++++++++++++ .../src/hooks/staker/useInitiateWithdraw.ts | 47 +++++++- staking-dashboard/src/utils/factoryHelpers.ts | 39 ++++++ 23 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 staking-dashboard/src/components/MilestoneStatusBadge/MilestoneStatusBadge.tsx create mode 100644 staking-dashboard/src/components/MilestoneStatusBadge/index.ts create mode 100644 staking-dashboard/src/hooks/atpRegistry/useMilestoneStatus.ts create mode 100644 staking-dashboard/src/utils/factoryHelpers.ts diff --git a/.gitignore b/.gitignore index 2f22bb934..ddcbb911d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/ .env.local .env.*.local .env.docker +.env.bak # IDE .idea/ diff --git a/atp-indexer/.env.example b/atp-indexer/.env.example index 7a77f2146..fe0ddd778 100644 --- a/atp-indexer/.env.example +++ b/atp-indexer/.env.example @@ -1,5 +1,5 @@ -# Database URL -DATABASE_URL=postgresql://user:password@localhost:5432/ponder +# Database (NOTE: Use POSTGRES_CONNECTION_STRING, not DATABASE_URL) +POSTGRES_CONNECTION_STRING=postgresql://user:password@localhost:5432/ponder # RPC URL RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY @@ -10,11 +10,16 @@ CHAIN_ID=1 # Contract Addresses ATP_FACTORY_ADDRESS=0x... ATP_FACTORY_AUCTION_ADDRESS=0x... +ATP_FACTORY_EMPLOYEE_ADDRESS=0x... +ATP_FACTORY_INVESTOR_ADDRESS=0x... STAKING_REGISTRY_ADDRESS=0x... ROLLUP_ADDRESS=0x... # Indexer Settings START_BLOCK=0 +# Per-factory start blocks (optional, for efficiency - set to deployment block of each factory) +EMPLOYEE_FACTORY_START_BLOCK=0 +INVESTOR_FACTORY_START_BLOCK=0 # Application NODE_ENV=development diff --git a/atp-indexer/bootstrap.sh b/atp-indexer/bootstrap.sh index 4eeec27b2..9cf09a72e 100755 --- a/atp-indexer/bootstrap.sh +++ b/atp-indexer/bootstrap.sh @@ -74,6 +74,10 @@ get_contract_addresses() { ATP_REGISTRY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpRegistryAuction') ATP_FACTORY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryAuction') + # new factories + ATP_FACTORY_EMPLOYEE_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryEmployee') + ATP_FACTORY_INVESTOR_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryInvestor') + # other STAKING_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.stakingRegistry') ROLLUP_ADDRESS=$(cat $contract_addresses_file | jq -r '.rollupAddress') @@ -150,6 +154,8 @@ CHAIN_ID=${CHAIN_ID} # Contract addresses ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS} ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} +ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS} +ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} ROLLUP_ADDRESS=${ROLLUP_ADDRESS} @@ -187,6 +193,8 @@ CHAIN_ID=${CHAIN_ID} # Contract addresses ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS} ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} +ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS} +ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} ROLLUP_ADDRESS=${ROLLUP_ADDRESS} @@ -469,6 +477,7 @@ case $ACTION in echo "" echo " Required contract address variables:" echo " ATP_FACTORY_ADDRESS, ATP_FACTORY_AUCTION_ADDRESS" + echo " ATP_FACTORY_EMPLOYEE_ADDRESS, ATP_FACTORY_INVESTOR_ADDRESS" echo " ATP_REGISTRY_ADDRESS, ATP_REGISTRY_AUCTION_ADDRESS" echo " STAKING_REGISTRY_ADDRESS, ROLLUP_ADDRESS" echo " START_BLOCK (optional, defaults to 0)" diff --git a/atp-indexer/ponder.config.ts b/atp-indexer/ponder.config.ts index 240b161be..f4a9c3569 100644 --- a/atp-indexer/ponder.config.ts +++ b/atp-indexer/ponder.config.ts @@ -13,6 +13,14 @@ const ATPCreatedEvent = parseAbiItem( "event ATPCreated(address indexed beneficiary, address indexed atp, uint256 allocation)" ); +// Per-factory start blocks for efficient indexing +const FACTORY_START_BLOCKS = { + genesis: config.START_BLOCK || 0, + auction: config.START_BLOCK || 0, + employee: config.EMPLOYEE_FACTORY_START_BLOCK || config.START_BLOCK || 0, + investor: config.INVESTOR_FACTORY_START_BLOCK || config.START_BLOCK || 0, +}; + let databaseConfig: DatabaseConfig | undefined; @@ -48,14 +56,14 @@ export default createConfig({ }, contracts: { /** - * ATP Factory - Main contract + * ATP Factory - Genesis Sale contract * Emits ATPCreated events when new ATP positions are created */ ATPFactory: { chain: config.networkName, abi: ATP_ABI, address: config.ATP_FACTORY_ADDRESS as `0x${string}`, - startBlock: config.START_BLOCK, + startBlock: FACTORY_START_BLOCKS.genesis, }, /** @@ -66,7 +74,29 @@ export default createConfig({ chain: config.networkName, abi: ATP_ABI, address: config.ATP_FACTORY_AUCTION_ADDRESS as `0x${string}`, - startBlock: config.START_BLOCK, + startBlock: FACTORY_START_BLOCKS.auction, + }, + + /** + * ATP Factory - Employee contract + * Issues MATPs to employees + */ + ATPFactoryEmployee: { + chain: config.networkName, + abi: ATP_ABI, + address: config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`, + startBlock: FACTORY_START_BLOCKS.employee, + }, + + /** + * ATP Factory - Investor contract + * Issues LATPs and MATPs to investors + */ + ATPFactoryInvestor: { + chain: config.networkName, + abi: ATP_ABI, + address: config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`, + startBlock: FACTORY_START_BLOCKS.investor, }, /** @@ -94,7 +124,7 @@ export default createConfig({ /** * Dynamic ATP Contracts * Created by factory events, tracks operator updates - * Uses factory pattern to only index ATPs created by our factories + * Uses factory pattern to only index ATPs created by all 4 factories */ ATP: { chain: config.networkName, @@ -103,11 +133,14 @@ export default createConfig({ address: [ config.ATP_FACTORY_ADDRESS as `0x${string}`, config.ATP_FACTORY_AUCTION_ADDRESS as `0x${string}`, + config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`, + config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`, ], event: ATPCreatedEvent, parameter: "atp", }), - startBlock: config.START_BLOCK, + // Use earliest factory start block to capture all ATP contracts + startBlock: Math.min(...Object.values(FACTORY_START_BLOCKS)), }, /** diff --git a/atp-indexer/ponder.schema.ts b/atp-indexer/ponder.schema.ts index 58170d559..d9200aee5 100644 --- a/atp-indexer/ponder.schema.ts +++ b/atp-indexer/ponder.schema.ts @@ -15,6 +15,7 @@ export const atpPosition = onchainTable("atp_position", (t) => ({ type: atpType("type").notNull(), stakerAddress: t.hex().notNull(), operatorAddress: t.hex(), + factoryAddress: t.hex().notNull(), // Factory that created this ATP blockNumber: t.bigint().notNull(), txHash: t.hex().notNull(), logIndex: t.integer().notNull(), @@ -23,6 +24,7 @@ export const atpPosition = onchainTable("atp_position", (t) => ({ addressIdx: index().on(table.address), beneficiaryIdx: index().on(table.beneficiary), stakerAddressIdx: index().on(table.stakerAddress), + factoryAddressIdx: index().on(table.factoryAddress), })); export const atpPositionRelations = relations(atpPosition, ({ many }) => ({ diff --git a/atp-indexer/src/api/handlers/atp/beneficiary.ts b/atp-indexer/src/api/handlers/atp/beneficiary.ts index 925bcc21e..07d2aabff 100644 --- a/atp-indexer/src/api/handlers/atp/beneficiary.ts +++ b/atp-indexer/src/api/handlers/atp/beneficiary.ts @@ -144,6 +144,7 @@ export async function handleATPByBeneficiary(c: Context): Promise { allocation: pos.allocation.toString(), type: pos.type, stakerAddress: checksumAddress(pos.stakerAddress), + factoryAddress: checksumAddress(pos.factoryAddress), sequentialNumber: index + 1, timestamp: Number(pos.timestamp), totalWithdrawn: (withdrawalMap.get(normalizedAddress) ?? 0n).toString(), diff --git a/atp-indexer/src/api/types/atp.types.ts b/atp-indexer/src/api/types/atp.types.ts index 033ac0d9b..816b5bd3f 100644 --- a/atp-indexer/src/api/types/atp.types.ts +++ b/atp-indexer/src/api/types/atp.types.ts @@ -56,6 +56,7 @@ export interface ATPPosition { allocation: string; type: string; stakerAddress: string; + factoryAddress: string; // Factory that created this ATP sequentialNumber: number; timestamp: number; totalWithdrawn?: string; diff --git a/atp-indexer/src/config/index.ts b/atp-indexer/src/config/index.ts index 4ec956f30..570aa4c99 100644 --- a/atp-indexer/src/config/index.ts +++ b/atp-indexer/src/config/index.ts @@ -48,11 +48,15 @@ const configSchema = z.object({ // Contract addresses ATP_FACTORY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), ATP_FACTORY_AUCTION_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), + ATP_FACTORY_EMPLOYEE_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), + ATP_FACTORY_INVESTOR_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), STAKING_REGISTRY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), ROLLUP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), // Indexer settings START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'START_BLOCK must be non-negative').default('0'), + EMPLOYEE_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'EMPLOYEE_FACTORY_START_BLOCK must be non-negative').optional(), + INVESTOR_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'INVESTOR_FACTORY_START_BLOCK must be non-negative').optional(), // Application NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), diff --git a/atp-indexer/src/events/atp-factory/atp-created.ts b/atp-indexer/src/events/atp-factory/atp-created.ts index d2520bdaa..996a5025a 100644 --- a/atp-indexer/src/events/atp-factory/atp-created.ts +++ b/atp-indexer/src/events/atp-factory/atp-created.ts @@ -62,6 +62,7 @@ async function handleATPCreated({ event, context }: IndexingFunctionArgs<'ATPFac const atpType = await determineATPType(atp, client); const stakerAddress = await getStakerAddress(atp, client); + const factoryAddress = event.log.address; // Factory contract that emitted the event await db.insert(atpPosition).values({ id: normalizeAddress(atp), @@ -71,19 +72,28 @@ async function handleATPCreated({ event, context }: IndexingFunctionArgs<'ATPFac type: atpType, stakerAddress: normalizeAddress(stakerAddress) as `0x${string}`, operatorAddress: null, + factoryAddress: normalizeAddress(factoryAddress) as `0x${string}`, blockNumber: event.block.number, txHash: event.transaction.hash, logIndex: event.log.logIndex, timestamp: event.block.timestamp, }) - console.log(`${atpType} created (${source}): ${atp}`); + console.log(`${atpType} created (${source}): ${atp} from factory ${factoryAddress}`); } ponder.on("ATPFactory:ATPCreated", async (params) => { - await handleATPCreated(params, "factory"); + await handleATPCreated(params, "genesis"); }); ponder.on("ATPFactoryAuction:ATPCreated", async (params) => { await handleATPCreated(params, "auction"); }); + +ponder.on("ATPFactoryEmployee:ATPCreated", async (params) => { + await handleATPCreated(params, "employee"); +}); + +ponder.on("ATPFactoryInvestor:ATPCreated", async (params) => { + await handleATPCreated(params, "investor"); +}); diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx index 9fe813304..64e5f9f02 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx @@ -32,6 +32,10 @@ interface ATPDetailsDelegationItemProps { providerRewardsRecipient: string }) => void onWithdrawSuccess?: () => void + // ATP context for milestone validation + atpType?: string + registryAddress?: Address + milestoneId?: bigint } /** @@ -45,7 +49,10 @@ export const ATPDetailsDelegationItem = ({ stakerAddress, rollupVersion, onClaimClick, - onWithdrawSuccess + onWithdrawSuccess, + atpType, + registryAddress, + milestoneId }: ATPDetailsDelegationItemProps) => { const [isExpanded, setIsExpanded] = useState(false) const { symbol, decimals } = useStakingAssetTokenDetails() @@ -488,6 +495,9 @@ export const ATPDetailsDelegationItem = ({ refetchStatus() onWithdrawSuccess?.() }} + atpType={atpType} + registryAddress={registryAddress} + milestoneId={milestoneId} /> )} diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx index d2dc10dfb..fa0ce6534 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx @@ -26,13 +26,17 @@ interface ATPDetailsDirectStakeItemProps { atp: ATPData onClaimSuccess?: () => void onWithdrawSuccess?: () => void + // ATP context for milestone validation + atpType?: string + registryAddress?: Address + milestoneId?: bigint } /** * Individual self stake item component * Displays sequencer address, transaction info, and links to explorers */ -export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, atp, onClaimSuccess, onWithdrawSuccess }: ATPDetailsDirectStakeItemProps) => { +export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, atp, onClaimSuccess, onWithdrawSuccess, atpType, registryAddress, milestoneId }: ATPDetailsDirectStakeItemProps) => { const [isExpanded, setIsExpanded] = useState(false) const [isClaimModalOpen, setIsClaimModalOpen] = useState(false) const { symbol, decimals } = useStakingAssetTokenDetails() @@ -393,6 +397,9 @@ export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, refetchStatus() onWithdrawSuccess?.() }} + atpType={atpType} + registryAddress={registryAddress} + milestoneId={milestoneId} /> )} diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx index 71972c3e7..2643774d0 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx @@ -22,6 +22,7 @@ import { ClaimAllProvider } from "@/contexts/ClaimAllContext" import { ClaimAllDelegationRewardsButton } from "@/components/ClaimAllDelegationRewardsButton" import { ClaimDelegationRewardsModal, type DelegationModalData } from "@/components/ClaimDelegationRewardsModal" import type { ATPData } from "@/hooks/atp" +import { isMATPData } from "@/hooks/atp/matp/matpTypes" import type { Address } from "viem" interface ATPDetailsModalProps { @@ -263,6 +264,11 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef const { messages: alertMessages, type: alertType } = alertData + // Extract ATP context for milestone validation + const atpType = atp.typeString; // "MATP", "LATP", "NCATP" + const registryAddress = atp.registry as Address; + const milestoneId = isMATPData(atp) ? atp.milestoneId : undefined; + return createPortal(
))}
@@ -458,6 +467,9 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef rollupVersion={rollupVersion} onClaimClick={handleDelegationClaimClick} onWithdrawSuccess={handleWithdrawSuccess} + atpType={atpType} + registryAddress={registryAddress} + milestoneId={milestoneId} /> ))} diff --git a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx index 1df37bc5c..51e1a2b9f 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx @@ -6,6 +6,7 @@ import { TooltipIcon } from "@/components/Tooltip"; import { SequencerStatus } from "@/hooks/rollup/useSequencerStatus"; import { useAlert } from "@/contexts/AlertContext"; import { getUnlockTimeDisplay } from "@/utils/dateFormatters"; +import { MilestoneStatusBadge } from "@/components/MilestoneStatusBadge"; /** * Parse contract errors to extract user-friendly messages @@ -79,6 +80,10 @@ interface WithdrawalActionsProps { actualUnlockTime?: bigint; withdrawalDelayDays?: number; onSuccess?: () => void; + // ATP context for milestone validation + atpType?: string; + registryAddress?: Address; + milestoneId?: bigint; } /** @@ -94,6 +99,10 @@ export const WithdrawalActions = ({ actualUnlockTime, withdrawalDelayDays, onSuccess, + // ATP context + atpType, + registryAddress, + milestoneId, }: WithdrawalActionsProps) => { const { showAlert } = useAlert(); const isExiting = status === SequencerStatus.EXITING; @@ -104,7 +113,15 @@ export const WithdrawalActions = ({ isConfirming: isConfirmingInitiate, isSuccess: isInitiateSuccess, error: initiateError, - } = useInitiateWithdraw(stakerAddress); + milestoneStatus, + isMilestoneLoading, + canWithdraw, + milestoneBlockError, + } = useInitiateWithdraw(stakerAddress, { + registryAddress, + milestoneId, + atpType, + }); const { finalizeWithdraw, @@ -114,8 +131,17 @@ export const WithdrawalActions = ({ error: finalizeError, } = useFinalizeWithdraw(); + // Determine if milestone gates operations + const isMATP = atpType === 'MATP'; + const isMilestoneGated = isMATP && !canWithdraw; + const canInitiateUnstake = - status === SequencerStatus.VALIDATING || status === SequencerStatus.ZOMBIE; + (status === SequencerStatus.VALIDATING || status === SequencerStatus.ZOMBIE) + && !isMilestoneGated; // Block if milestone not succeeded + + const canFinalizeWithdrawNow = + canFinalize + && !isMilestoneGated; // Block if milestone not succeeded // Handle initiate withdraw errors useEffect(() => { @@ -178,6 +204,25 @@ export const WithdrawalActions = ({ maxWidth="max-w-md" /> + + {/* Show milestone status for MATPs */} + {isMATP && ( +
+ +
+ )} + + {/* Show milestone error message */} + {milestoneBlockError && ( +
+
+ {milestoneBlockError} +
+
+ )}
)}
diff --git a/staking-dashboard/src/components/MilestoneStatusBadge/MilestoneStatusBadge.tsx b/staking-dashboard/src/components/MilestoneStatusBadge/MilestoneStatusBadge.tsx new file mode 100644 index 000000000..ccb99618c --- /dev/null +++ b/staking-dashboard/src/components/MilestoneStatusBadge/MilestoneStatusBadge.tsx @@ -0,0 +1,54 @@ +import { Tooltip } from "@/components/Tooltip"; +import { + MilestoneStatus, + getMilestoneStatusText, + getMilestoneStatusColors, +} from "@/hooks/atpRegistry/useMilestoneStatus"; + +interface MilestoneStatusBadgeProps { + status?: MilestoneStatus; + isLoading?: boolean; + showTooltip?: boolean; +} + +export const MilestoneStatusBadge = ({ + status, + isLoading, + showTooltip = true +}: MilestoneStatusBadgeProps) => { + if (isLoading) { + return ( + + Loading... + + ); + } + + if (status === undefined) return null; + + const statusText = getMilestoneStatusText(status); + const colors = getMilestoneStatusColors(status); + + const tooltipContent = { + [MilestoneStatus.Pending]: + "This milestone has not been reached yet. Withdrawals are disabled.", + [MilestoneStatus.Failed]: + "This milestone was not achieved. Control has transferred to Aztec Labs.", + [MilestoneStatus.Succeeded]: + "This milestone has been successfully achieved. All operations are available.", + }[status]; + + const badge = ( + + + Milestone: {statusText} + + ); + + if (!showTooltip) return badge; + + return {badge}; +}; diff --git a/staking-dashboard/src/components/MilestoneStatusBadge/index.ts b/staking-dashboard/src/components/MilestoneStatusBadge/index.ts new file mode 100644 index 000000000..8f6399684 --- /dev/null +++ b/staking-dashboard/src/components/MilestoneStatusBadge/index.ts @@ -0,0 +1 @@ +export { MilestoneStatusBadge } from "./MilestoneStatusBadge"; diff --git a/staking-dashboard/src/hooks/atp/atpBaseTypes.ts b/staking-dashboard/src/hooks/atp/atpBaseTypes.ts index 8939a1532..7a4b58a64 100644 --- a/staking-dashboard/src/hooks/atp/atpBaseTypes.ts +++ b/staking-dashboard/src/hooks/atp/atpBaseTypes.ts @@ -36,6 +36,7 @@ export const BaseATPSchema = z.object({ sequentialNumber: z.number().optional(), totalWithdrawn: z.bigint().optional(), totalSlashed: z.bigint().optional(), + factoryAddress: AddressSchema.optional(), // Factory that created this ATP }); // Base ATP data type diff --git a/staking-dashboard/src/hooks/atp/useAtpHoldings.ts b/staking-dashboard/src/hooks/atp/useAtpHoldings.ts index e5e651ef7..0a1746924 100644 --- a/staking-dashboard/src/hooks/atp/useAtpHoldings.ts +++ b/staking-dashboard/src/hooks/atp/useAtpHoldings.ts @@ -9,6 +9,7 @@ export interface ATPHolding { allocation: string; beneficiary: string; stakerAddress: string; + factoryAddress: string; // Factory that created this ATP sequentialNumber: number; timestamp: number; totalWithdrawn: string; diff --git a/staking-dashboard/src/hooks/atp/useMultipleAtpData.ts b/staking-dashboard/src/hooks/atp/useMultipleAtpData.ts index 1454349db..e3f4e8f33 100644 --- a/staking-dashboard/src/hooks/atp/useMultipleAtpData.ts +++ b/staking-dashboard/src/hooks/atp/useMultipleAtpData.ts @@ -62,14 +62,14 @@ function buildAtpData( if (holding.type === 'MATP') { const data = buildMATPData(address, results); - return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed }; + return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed, factoryAddress: holding.factoryAddress as `0x${string}` }; } if (holding.type === 'LATP') { // Not overriding the global lock because the ATPRegistryAuction global lock params already returns the correct timestamp // https://etherscan.io/address/0x63841bAD6B35b6419e15cA9bBBbDf446D4dC3dde#readContract const data = buildLATPData(address, results); - return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed }; + return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed, factoryAddress: holding.factoryAddress as `0x${string}` }; } if (holding.type === 'NCATP') { @@ -82,7 +82,7 @@ function buildAtpData( withdrawalTimestamp: overrides?.withdrawalTimestamp, hasStaked: overrides?.hasStaked }); - return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed }; + return { ...data, sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed, factoryAddress: holding.factoryAddress as `0x${string}` }; } // Unknown type fallback @@ -103,6 +103,7 @@ function buildAtpData( sequentialNumber: holding.sequentialNumber, totalWithdrawn, totalSlashed, + factoryAddress: holding.factoryAddress as `0x${string}`, } as ATPData; } diff --git a/staking-dashboard/src/hooks/atpRegistry/index.ts b/staking-dashboard/src/hooks/atpRegistry/index.ts index 93c372bbd..130c601c7 100644 --- a/staking-dashboard/src/hooks/atpRegistry/index.ts +++ b/staking-dashboard/src/hooks/atpRegistry/index.ts @@ -4,6 +4,7 @@ export { useAtpRegistryData, isAuctionRegistry } from "./useAtpRegistryData"; // Parameterized read hooks export { useStakerImplementation } from "./useStakerImplementation"; export { useStakerImplementations } from "./useStakerImplementations"; +export * from "./useMilestoneStatus"; // Write hook - keep separate for transactions export { useSetExecuteAllowedAt } from "./useSetExecuteAllowedAt"; diff --git a/staking-dashboard/src/hooks/atpRegistry/useMilestoneStatus.ts b/staking-dashboard/src/hooks/atpRegistry/useMilestoneStatus.ts new file mode 100644 index 000000000..780971b31 --- /dev/null +++ b/staking-dashboard/src/hooks/atpRegistry/useMilestoneStatus.ts @@ -0,0 +1,111 @@ +import { useReadContract } from "wagmi"; +import type { Address } from "viem"; +import { AtpRegistryAbi } from "../../contracts/abis/ATPRegistry"; + +/** + * Milestone status enum matching Registry.sol + * CRITICAL: Use "Succeeded" (value 2), NOT "Reached" + */ +export enum MilestoneStatus { + Pending = 0, // Milestone not yet reached + Failed = 1, // Milestone failed + Succeeded = 2, // Milestone achieved - ONLY this allows operations +} + +interface UseMilestoneStatusParams { + registryAddress: Address | undefined; + milestoneId?: bigint; + enabled?: boolean; +} + +/** + * Hook to fetch milestone status from ATP Registry + */ +export function useMilestoneStatus({ + registryAddress, + milestoneId, + enabled = true, +}: UseMilestoneStatusParams) { + const milestoneStatusQuery = useReadContract({ + abi: AtpRegistryAbi, + address: registryAddress, + functionName: "getMilestoneStatus", + args: milestoneId !== undefined ? [milestoneId] : undefined, + query: { + enabled: enabled && registryAddress !== undefined && milestoneId !== undefined, + refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }, + }); + + return { + status: milestoneStatusQuery.data as MilestoneStatus | undefined, + isLoading: milestoneStatusQuery.isLoading, + error: milestoneStatusQuery.error, + refetch: milestoneStatusQuery.refetch, + }; +} + +/** + * Helper to check if milestone allows withdrawal + */ +export function canWithdrawWithMilestone(status?: MilestoneStatus): boolean { + return status === MilestoneStatus.Succeeded; +} + +/** + * Helper to get human-readable status text + */ +export function getMilestoneStatusText(status?: MilestoneStatus): string { + switch (status) { + case MilestoneStatus.Pending: + return "Pending"; + case MilestoneStatus.Failed: + return "Failed"; + case MilestoneStatus.Succeeded: + return "Achieved"; // User-friendly text + default: + return "Unknown"; + } +} + +/** + * Helper to get status colors (using actual theme colors) + */ +export function getMilestoneStatusColors(status?: MilestoneStatus): { + text: string; + bg: string; + border: string; + indicator: string; +} { + switch (status) { + case MilestoneStatus.Pending: + return { + text: "text-aqua", + bg: "bg-aqua/10", + border: "border-aqua/40", + indicator: "bg-aqua", + }; + case MilestoneStatus.Failed: + return { + text: "text-vermillion", + bg: "bg-vermillion/10", + border: "border-vermillion/40", + indicator: "bg-vermillion", + }; + case MilestoneStatus.Succeeded: + return { + text: "text-chartreuse", + bg: "bg-chartreuse/10", + border: "border-chartreuse/40", + indicator: "bg-chartreuse", + }; + default: + return { + text: "text-parchment/60", + bg: "bg-parchment/5", + border: "border-parchment/20", + indicator: "bg-parchment/40", + }; + } +} diff --git a/staking-dashboard/src/hooks/staker/useInitiateWithdraw.ts b/staking-dashboard/src/hooks/staker/useInitiateWithdraw.ts index e0332aee2..3af3bc5c0 100644 --- a/staking-dashboard/src/hooks/staker/useInitiateWithdraw.ts +++ b/staking-dashboard/src/hooks/staker/useInitiateWithdraw.ts @@ -1,20 +1,56 @@ import { useWriteContract, useWaitForTransactionReceipt } from "@/hooks/useWagmiStrategy" import type { Address } from "viem" import { ATPWithdrawableStakerAbi } from "@/contracts/abis/ATPWithdrawableStaker" +import { + useMilestoneStatus, + canWithdrawWithMilestone, + getMilestoneStatusText, +} from "@/hooks/atpRegistry/useMilestoneStatus" + +interface UseInitiateWithdrawOptions { + registryAddress?: Address; + milestoneId?: bigint; + atpType?: string; +} /** * Hook to initiate withdrawal from the rollup for a delegation * @param stakerAddress - Address of the withdrawable staker contract + * @param options - Optional milestone validation parameters * @returns Hook with initiateWithdraw function and transaction status */ -export function useInitiateWithdraw(stakerAddress: Address) { - const { data: hash, writeContract, isPending, error } = useWriteContract() +export function useInitiateWithdraw( + stakerAddress: Address, + options?: UseInitiateWithdrawOptions +) { + const { data: hash, writeContract, isPending, error: writeError } = useWriteContract() const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash, }) + // Check milestone status for MATPs + const { + status: milestoneStatus, + isLoading: isMilestoneLoading, + error: milestoneError, + } = useMilestoneStatus({ + registryAddress: options?.registryAddress, + milestoneId: options?.milestoneId, + enabled: options?.atpType === 'MATP' && !!options?.registryAddress, + }); + + const isMATP = options?.atpType === 'MATP'; + const canWithdraw = !isMATP || canWithdrawWithMilestone(milestoneStatus); + + // Build error message if milestone blocks withdrawal + const milestoneBlockError = isMATP && !canWithdraw + ? `Cannot withdraw: milestone status is ${getMilestoneStatusText(milestoneStatus)}. ` + + `Withdrawals require milestone to be achieved (Succeeded status).` + : null; + const initiateWithdraw = (version: bigint, attesterAddress: Address) => { + // Don't throw - let UI handle via disabled state return writeContract({ abi: ATPWithdrawableStakerAbi, address: stakerAddress, @@ -28,7 +64,12 @@ export function useInitiateWithdraw(stakerAddress: Address) { isPending, isConfirming, isSuccess, - error, + error: writeError || milestoneError, hash, + // Milestone-specific state for UI + milestoneStatus, + isMilestoneLoading, + canWithdraw, + milestoneBlockError, // Explicit error message for UI } } diff --git a/staking-dashboard/src/utils/factoryHelpers.ts b/staking-dashboard/src/utils/factoryHelpers.ts new file mode 100644 index 000000000..aea9648c8 --- /dev/null +++ b/staking-dashboard/src/utils/factoryHelpers.ts @@ -0,0 +1,39 @@ +import type { Address } from "viem"; + +/** + * Map of factory addresses to human-readable names + * These are network-specific addresses + */ +export const FACTORY_NAMES: Record = { + // Mainnet factories + "0xaa292e8611adf267e563f334ee42320ac96d0463": "Genesis Sale", + "0x3155755b79aa083bd953911c92705b7aa82a18f9": "Auction", + "0xa17ea96757c9bb9b41a12ef5073c51129937ffae": "Employee", + "0x278f39b11b3de0796561e85cb48535c9f45ddfcc": "Investor", + + // Anvil/Dev factories + "0xd6e1afe5ca8d00a2efc01b89997abe2de47fdfaf": "Employee", + "0x6f6f570f45833e249e27022648a26f4076f48f78": "Investor", +}; + +/** + * Get human-readable factory name from factory address + * Falls back to "Unknown" if factory address is not recognized + */ +export function getFactoryName(factoryAddress?: Address | string): string { + if (!factoryAddress) { + return "Unknown"; + } + + const normalized = factoryAddress.toLowerCase(); + return FACTORY_NAMES[normalized] || "Unknown"; +} + +/** + * Get short factory identifier (first word only) + * Used for compact displays + */ +export function getFactoryShortName(factoryAddress?: Address | string): string { + const fullName = getFactoryName(factoryAddress); + return fullName.split(" ")[0]; // Returns "Genesis", "Auction", "Employee", "Investor", or "Unknown" +} From 1339a110b47189ae49bf508d34739d41b1355ade Mon Sep 17 00:00:00 2001 From: Amin Sammara Date: Wed, 25 Feb 2026 23:24:47 -0500 Subject: [PATCH 2/4] refactor: rename EMPLOYEE/INVESTOR to MATP/LATP and fix vesting graph - Rename factory terminology from Employee/Investor to MATP/LATP across indexer config, bootstrap scripts, terraform, and CI workflows - Delete factoryHelpers.ts with hardcoded addresses - Fix VestingGraph to use blockchain timestamp instead of Date.now() - Expand vesting graph X-axis when NOW is before vesting start - Add horizontal dotted line showing current vested amount - Display milestone ID on MATP cards - Show "Milestone still locked" for MATPs where time passed but milestone not succeeded - Remove sensitive references from milestone badge tooltips --- .github/workflows/deploy-indexer.yaml | 2 + atp-indexer/.env.example | 8 +- atp-indexer/bootstrap.sh | 16 +- atp-indexer/ponder.config.ts | 28 +- atp-indexer/src/config/index.ts | 8 +- .../src/events/atp-factory/atp-created.ts | 8 +- atp-indexer/terraform/app.tf | 4 + atp-indexer/terraform/variables.tf | 12 + .../ATPDetailsModal/ATPDetailsModal.tsx | 3 + .../ATPStakingCard/ATPStakingCard.tsx | 41 ++- .../MilestoneStatusBadge.tsx | 4 +- .../VestingSchedule/VestingGraph.tsx | 333 ++++++++++-------- .../src/hooks/atp/useVestingCalculation.ts | 10 +- staking-dashboard/src/utils/atpFormatters.ts | 9 +- staking-dashboard/src/utils/factoryHelpers.ts | 39 -- 15 files changed, 279 insertions(+), 246 deletions(-) delete mode 100644 staking-dashboard/src/utils/factoryHelpers.ts diff --git a/.github/workflows/deploy-indexer.yaml b/.github/workflows/deploy-indexer.yaml index 0ab6d1dac..783e03f18 100644 --- a/.github/workflows/deploy-indexer.yaml +++ b/.github/workflows/deploy-indexer.yaml @@ -56,6 +56,8 @@ jobs: # Contract Addresses (from GitHub environment variables) ATP_FACTORY_ADDRESS: ${{ vars.ATP_FACTORY_ADDRESS }} ATP_FACTORY_AUCTION_ADDRESS: ${{ vars.ATP_FACTORY_AUCTION_ADDRESS }} + ATP_FACTORY_MATP_ADDRESS: ${{ vars.ATP_FACTORY_MATP_ADDRESS }} + ATP_FACTORY_LATP_ADDRESS: ${{ vars.ATP_FACTORY_LATP_ADDRESS }} ATP_REGISTRY_ADDRESS: ${{ vars.ATP_REGISTRY_ADDRESS }} ATP_REGISTRY_AUCTION_ADDRESS: ${{ vars.ATP_REGISTRY_AUCTION_ADDRESS }} STAKING_REGISTRY_ADDRESS: ${{ vars.STAKING_REGISTRY_ADDRESS }} diff --git a/atp-indexer/.env.example b/atp-indexer/.env.example index fe0ddd778..54b65d1d7 100644 --- a/atp-indexer/.env.example +++ b/atp-indexer/.env.example @@ -10,16 +10,16 @@ CHAIN_ID=1 # Contract Addresses ATP_FACTORY_ADDRESS=0x... ATP_FACTORY_AUCTION_ADDRESS=0x... -ATP_FACTORY_EMPLOYEE_ADDRESS=0x... -ATP_FACTORY_INVESTOR_ADDRESS=0x... +ATP_FACTORY_MATP_ADDRESS=0x... +ATP_FACTORY_LATP_ADDRESS=0x... STAKING_REGISTRY_ADDRESS=0x... ROLLUP_ADDRESS=0x... # Indexer Settings START_BLOCK=0 # Per-factory start blocks (optional, for efficiency - set to deployment block of each factory) -EMPLOYEE_FACTORY_START_BLOCK=0 -INVESTOR_FACTORY_START_BLOCK=0 +MATP_FACTORY_START_BLOCK=0 +LATP_FACTORY_START_BLOCK=0 # Application NODE_ENV=development diff --git a/atp-indexer/bootstrap.sh b/atp-indexer/bootstrap.sh index 9cf09a72e..a2e26f3ec 100755 --- a/atp-indexer/bootstrap.sh +++ b/atp-indexer/bootstrap.sh @@ -75,8 +75,8 @@ get_contract_addresses() { ATP_FACTORY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryAuction') # new factories - ATP_FACTORY_EMPLOYEE_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryEmployee') - ATP_FACTORY_INVESTOR_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryInvestor') + ATP_FACTORY_MATP_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryMatp') + ATP_FACTORY_LATP_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryLatp') # other STAKING_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.stakingRegistry') @@ -154,8 +154,8 @@ CHAIN_ID=${CHAIN_ID} # Contract addresses ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS} ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} -ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS} -ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS} +ATP_FACTORY_MATP_ADDRESS=${ATP_FACTORY_MATP_ADDRESS} +ATP_FACTORY_LATP_ADDRESS=${ATP_FACTORY_LATP_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} ROLLUP_ADDRESS=${ROLLUP_ADDRESS} @@ -193,8 +193,8 @@ CHAIN_ID=${CHAIN_ID} # Contract addresses ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS} ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} -ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS} -ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS} +ATP_FACTORY_MATP_ADDRESS=${ATP_FACTORY_MATP_ADDRESS} +ATP_FACTORY_LATP_ADDRESS=${ATP_FACTORY_LATP_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} ROLLUP_ADDRESS=${ROLLUP_ADDRESS} @@ -369,6 +369,8 @@ function deploy() { local args="-var=rpc_url=$RPC_URL \ -var=atp_factory_address=$ATP_FACTORY_ADDRESS \ -var=atp_factory_auction_address=$ATP_FACTORY_AUCTION_ADDRESS \ + -var=atp_factory_matp_address=$ATP_FACTORY_MATP_ADDRESS \ + -var=atp_factory_latp_address=$ATP_FACTORY_LATP_ADDRESS \ -var=staking_registry_address=$STAKING_REGISTRY_ADDRESS \ -var=rollup_address=$ROLLUP_ADDRESS \ -var=start_block=$START_BLOCK \ @@ -477,7 +479,7 @@ case $ACTION in echo "" echo " Required contract address variables:" echo " ATP_FACTORY_ADDRESS, ATP_FACTORY_AUCTION_ADDRESS" - echo " ATP_FACTORY_EMPLOYEE_ADDRESS, ATP_FACTORY_INVESTOR_ADDRESS" + echo " ATP_FACTORY_MATP_ADDRESS, ATP_FACTORY_LATP_ADDRESS" echo " ATP_REGISTRY_ADDRESS, ATP_REGISTRY_AUCTION_ADDRESS" echo " STAKING_REGISTRY_ADDRESS, ROLLUP_ADDRESS" echo " START_BLOCK (optional, defaults to 0)" diff --git a/atp-indexer/ponder.config.ts b/atp-indexer/ponder.config.ts index f4a9c3569..83326702b 100644 --- a/atp-indexer/ponder.config.ts +++ b/atp-indexer/ponder.config.ts @@ -17,8 +17,8 @@ const ATPCreatedEvent = parseAbiItem( const FACTORY_START_BLOCKS = { genesis: config.START_BLOCK || 0, auction: config.START_BLOCK || 0, - employee: config.EMPLOYEE_FACTORY_START_BLOCK || config.START_BLOCK || 0, - investor: config.INVESTOR_FACTORY_START_BLOCK || config.START_BLOCK || 0, + matp: config.MATP_FACTORY_START_BLOCK || config.START_BLOCK || 0, + latp: config.LATP_FACTORY_START_BLOCK || config.START_BLOCK || 0, }; @@ -78,25 +78,25 @@ export default createConfig({ }, /** - * ATP Factory - Employee contract - * Issues MATPs to employees + * ATP Factory - MATP contract + * Issues milestone-based ATPs (MATPs) */ - ATPFactoryEmployee: { + ATPFactoryMATP: { chain: config.networkName, abi: ATP_ABI, - address: config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`, - startBlock: FACTORY_START_BLOCKS.employee, + address: config.ATP_FACTORY_MATP_ADDRESS as `0x${string}`, + startBlock: FACTORY_START_BLOCKS.matp, }, /** - * ATP Factory - Investor contract - * Issues LATPs and MATPs to investors + * ATP Factory - LATP contract + * Issues linear vesting ATPs (LATPs) and MATPs */ - ATPFactoryInvestor: { + ATPFactoryLATP: { chain: config.networkName, abi: ATP_ABI, - address: config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`, - startBlock: FACTORY_START_BLOCKS.investor, + address: config.ATP_FACTORY_LATP_ADDRESS as `0x${string}`, + startBlock: FACTORY_START_BLOCKS.latp, }, /** @@ -133,8 +133,8 @@ export default createConfig({ address: [ config.ATP_FACTORY_ADDRESS as `0x${string}`, config.ATP_FACTORY_AUCTION_ADDRESS as `0x${string}`, - config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`, - config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`, + config.ATP_FACTORY_MATP_ADDRESS as `0x${string}`, + config.ATP_FACTORY_LATP_ADDRESS as `0x${string}`, ], event: ATPCreatedEvent, parameter: "atp", diff --git a/atp-indexer/src/config/index.ts b/atp-indexer/src/config/index.ts index 570aa4c99..97111f554 100644 --- a/atp-indexer/src/config/index.ts +++ b/atp-indexer/src/config/index.ts @@ -48,15 +48,15 @@ const configSchema = z.object({ // Contract addresses ATP_FACTORY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), ATP_FACTORY_AUCTION_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), - ATP_FACTORY_EMPLOYEE_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), - ATP_FACTORY_INVESTOR_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), + ATP_FACTORY_MATP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), + ATP_FACTORY_LATP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), STAKING_REGISTRY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), ROLLUP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), // Indexer settings START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'START_BLOCK must be non-negative').default('0'), - EMPLOYEE_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'EMPLOYEE_FACTORY_START_BLOCK must be non-negative').optional(), - INVESTOR_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'INVESTOR_FACTORY_START_BLOCK must be non-negative').optional(), + MATP_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'MATP_FACTORY_START_BLOCK must be non-negative').optional(), + LATP_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'LATP_FACTORY_START_BLOCK must be non-negative').optional(), // Application NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), diff --git a/atp-indexer/src/events/atp-factory/atp-created.ts b/atp-indexer/src/events/atp-factory/atp-created.ts index 996a5025a..c08a52678 100644 --- a/atp-indexer/src/events/atp-factory/atp-created.ts +++ b/atp-indexer/src/events/atp-factory/atp-created.ts @@ -90,10 +90,10 @@ ponder.on("ATPFactoryAuction:ATPCreated", async (params) => { await handleATPCreated(params, "auction"); }); -ponder.on("ATPFactoryEmployee:ATPCreated", async (params) => { - await handleATPCreated(params, "employee"); +ponder.on("ATPFactoryMATP:ATPCreated", async (params) => { + await handleATPCreated(params, "matp"); }); -ponder.on("ATPFactoryInvestor:ATPCreated", async (params) => { - await handleATPCreated(params, "investor"); +ponder.on("ATPFactoryLATP:ATPCreated", async (params) => { + await handleATPCreated(params, "latp"); }); diff --git a/atp-indexer/terraform/app.tf b/atp-indexer/terraform/app.tf index 54f6d13bf..e92ecbf0c 100644 --- a/atp-indexer/terraform/app.tf +++ b/atp-indexer/terraform/app.tf @@ -257,6 +257,8 @@ locals { { name = "ATP_FACTORY_ADDRESS", value = var.atp_factory_address }, { name = "STAKING_REGISTRY_ADDRESS", value = var.staking_registry_address }, { name = "ATP_FACTORY_AUCTION_ADDRESS", value = var.atp_factory_auction_address }, + { name = "ATP_FACTORY_MATP_ADDRESS", value = var.atp_factory_matp_address }, + { name = "ATP_FACTORY_LATP_ADDRESS", value = var.atp_factory_latp_address }, { name = "ROLLUP_ADDRESS", value = var.rollup_address }, { name = "POLLING_INTERVAL", value = var.polling_interval }, { name = "MAX_RETRIES", value = var.max_retries }, @@ -282,6 +284,8 @@ locals { { name = "ATP_FACTORY_ADDRESS", value = var.atp_factory_address }, { name = "STAKING_REGISTRY_ADDRESS", value = var.staking_registry_address }, { name = "ATP_FACTORY_AUCTION_ADDRESS", value = var.atp_factory_auction_address }, + { name = "ATP_FACTORY_MATP_ADDRESS", value = var.atp_factory_matp_address }, + { name = "ATP_FACTORY_LATP_ADDRESS", value = var.atp_factory_latp_address }, { name = "ROLLUP_ADDRESS", value = var.rollup_address }, { name = "POLLING_INTERVAL", value = var.polling_interval }, { name = "MAX_RETRIES", value = var.max_retries }, diff --git a/atp-indexer/terraform/variables.tf b/atp-indexer/terraform/variables.tf index 04a91488d..1a7b5fb8f 100644 --- a/atp-indexer/terraform/variables.tf +++ b/atp-indexer/terraform/variables.tf @@ -127,6 +127,18 @@ variable "atp_factory_auction_address" { default = "" } +variable "atp_factory_matp_address" { + description = "ATP Factory MATP contract address" + type = string + default = "" +} + +variable "atp_factory_latp_address" { + description = "ATP Factory LATP contract address" + type = string + default = "" +} + variable "staking_registry_address" { description = "Staking Registry contract address" type = string diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx index 2643774d0..4f8d81426 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsModal.tsx @@ -7,6 +7,7 @@ import { useRollupData } from "@/hooks/rollup/useRollupData" import { useStakingAssetTokenDetails } from "@/hooks/stakingRegistry" import { useATP } from "@/hooks/useATP" import { useUserGovernancePower, usePendingWithdrawals } from "@/hooks/governance" +import { useBlockTimestamp } from "@/hooks/useBlockTimestamp" import { formatTokenAmount } from "@/utils/atpFormatters" import { Icon } from "@/components/Icon" import { ATPDetailsHeader } from "./ATPDetailsHeader" @@ -125,6 +126,7 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef const { votingPower: governanceVotingPower, refetch: refetchGovernancePower } = useUserGovernancePower({ stakerAddress: atp.staker }) + const { blockTimestamp } = useBlockTimestamp() // Get pending governance withdrawals for this ATP (initiated but not yet finalized) const { pendingWithdrawals: governancePendingWithdrawals, refetch: refetchGovernancePendingWithdrawals } = usePendingWithdrawals({ @@ -321,6 +323,7 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef globalLock={atp.globalLock} atpType={atp.typeString} registryAddress={atp.registry} + blockTimestamp={blockTimestamp} />
)} diff --git a/staking-dashboard/src/components/ATPStakingCard/ATPStakingCard.tsx b/staking-dashboard/src/components/ATPStakingCard/ATPStakingCard.tsx index c355cdd70..df671feaa 100644 --- a/staking-dashboard/src/components/ATPStakingCard/ATPStakingCard.tsx +++ b/staking-dashboard/src/components/ATPStakingCard/ATPStakingCard.tsx @@ -29,7 +29,7 @@ import { } from "@/utils/atpFormatters"; import type { ATPData } from "@/hooks/atp"; import { isMATPData } from "@/hooks/atp/matp/matpTypes"; -import { useMilestoneStatus } from "@/hooks/atpRegistry/useMilestoneStatus"; +import { useMilestoneStatus, MilestoneStatus } from "@/hooks/atpRegistry/useMilestoneStatus"; import { MilestoneStatusBadge } from "@/components/MilestoneStatusBadge"; import { ERC20Abi } from "@/contracts/abis/ERC20"; @@ -186,7 +186,10 @@ export const ATPStakingCard = ({ isApproveConfirming, refetchAllowance, } = useATPClaim(data); - const globalLockTimeDisplay = getTimeToClaimForATP(data); + // Get cached block timestamp for withdrawal eligibility check (refreshes every 60s) + const { blockTimestamp } = useBlockTimestamp(); + + const globalLockTimeDisplay = getTimeToClaimForATP(data, blockTimestamp); const { activationThreshold } = useRollupData(); const { @@ -218,9 +221,6 @@ export const ATPStakingCard = ({ }, }); - // Get cached block timestamp for withdrawal eligibility check (refreshes every 60s) - const { blockTimestamp } = useBlockTimestamp(); - // Check NCATP staker status - uses block.timestamp for withdrawal eligibility const { hasStaked, @@ -302,9 +302,19 @@ export const ATPStakingCard = ({ return `${days} days`; }; + // For MATPs: if time lock has passed but milestone is not Succeeded, show "Milestone still locked" + const getMATPlockDisplay = (): string => { + if (globalLockTimeDisplay === "Available now" && milestoneStatus !== MilestoneStatus.Succeeded) { + return "Milestone still locked"; + } + return globalLockTimeDisplay; + }; + const timeToClaimDisplay = isNCATP ? getNCAtpUnlockDisplay() - : globalLockTimeDisplay; + : isMATP + ? getMATPlockDisplay() + : globalLockTimeDisplay; // Calculate remaining allocation after withdrawals and slashing // Total Funds = allocation - totalWithdrawn - totalSlashed @@ -486,10 +496,6 @@ export const ATPStakingCard = ({

Token Vault #{data.sequentialNumber || "?"}

- {/* Factory badge - Hidden for now, uncomment to show factory source */} - {/* - {getFactoryName(data.factoryAddress)} - */} {/* Milestone badge for MATPs */} {isMATP && ( - + <> + + {data.milestoneId !== undefined && ( + + Milestone {Number(data.milestoneId)} + + )} + )} { +export const VestingGraph = ({ globalLock, registryAddress, className = "", blockTimestamp }: VestingGraphProps) => { const { symbol, decimals } = useStakingAssetTokenDetails() + // Check if this is an ATP from auction registry + const isAuctionATP = isAuctionRegistry(registryAddress) + // Check for invalid time range const hasInvalidTimeRange = Number(globalLock.endTime) < Number(globalLock.startTime) @@ -46,7 +52,7 @@ export const VestingGraph = ({ globalLock, className = "" }: VestingGraphProps) ) } - const vestingData = useVestingCalculation(globalLock) + const vestingData = useVestingCalculation(globalLock, blockTimestamp) const graphData = useMemo(() => { // Graph dimensions @@ -60,53 +66,60 @@ export const VestingGraph = ({ globalLock, className = "" }: VestingGraphProps) const startTime = Number(globalLock.startTime) const cliffTime = Number(globalLock.cliff) const endTime = Number(globalLock.endTime) - const now = Math.floor(Date.now() / 1000) + // Use blockchain timestamp if provided, otherwise fall back to Date.now() + const now = blockTimestamp ? Number(blockTimestamp) : Math.floor(Date.now() / 1000) // Handle edge cases const startEqualsCliff = startTime === cliffTime const cliffEqualsEnd = cliffTime === endTime - // Calculate time range for X axis - const timeRange = endTime - startTime - const nowPosition = timeRange === 0 ? graphWidth / 2 : ((now - startTime) / timeRange) * graphWidth + // Calculate time range for X axis - include NOW if it's before startTime + const xAxisStart = Math.min(now, startTime) + const timeRange = endTime - xAxisStart + + // Calculate X positions relative to the full axis range + const nowPosition = timeRange === 0 ? 0 : ((now - xAxisStart) / timeRange) * graphWidth + const startX = timeRange === 0 ? 0 : ((startTime - xAxisStart) / timeRange) * graphWidth + + // Calculate cliff X position relative to full axis + const cliffX = timeRange === 0 ? startX : ((cliffTime - xAxisStart) / timeRange) * graphWidth // Create path points for cliff vesting pattern - handle collision cases + // Note: Path starts at startX (not 0) when NOW is before startTime let pathPoints = [] if (startEqualsCliff && cliffEqualsEnd) { // All three points are the same - instant vest pathPoints = [ - { x: 0, y: 0 }, // Start at 100% + { x: startX, y: 0 }, // Start at 100% { x: graphWidth, y: 0 } // End at 100% ] } else if (startEqualsCliff) { // Start = Cliff, but different from End - immediate cliff unlock then linear pathPoints = [ - { x: 0, y: graphHeight - (vestingData.cliffUnlockRatio * graphHeight) }, // Start at cliff % + { x: startX, y: graphHeight - (vestingData.cliffUnlockRatio * graphHeight) }, // Start at cliff % { x: graphWidth, y: 0 } // Linear to 100% at end ] } else if (cliffEqualsEnd) { // Cliff = End - stay at 0% until cliff, then jump to 100% - const cliffX = graphWidth pathPoints = [ - { x: 0, y: graphHeight }, // Start at 0% - { x: cliffX - 1, y: graphHeight }, // Stay at 0% until just before cliff - { x: cliffX, y: 0 } // Jump to 100% at cliff/end + { x: startX, y: graphHeight }, // Start at 0% + { x: graphWidth - 1, y: graphHeight }, // Stay at 0% until just before cliff + { x: graphWidth, y: 0 } // Jump to 100% at cliff/end ] } else { // Normal case - all three points are distinct - const cliffX = ((cliffTime - startTime) / timeRange) * graphWidth pathPoints = [ - { x: 0, y: graphHeight }, // Start at 0% + { x: startX, y: graphHeight }, // Start at 0% { x: cliffX, y: graphHeight }, // Stay at 0% until cliff { x: cliffX, y: graphHeight - (vestingData.cliffUnlockRatio * graphHeight) }, // Jump to cliff % { x: graphWidth, y: 0 } // Linear to 100% at end ] } - // Create SVG path + // Create SVG path - area starts at startX (which may be > 0 if now < startTime) const linePath = `M ${pathPoints.map(p => `${p.x},${p.y}`).join(' L ')}` - const areaPath = `M 0,${graphHeight} ${pathPoints.map(p => `L ${p.x},${p.y}`).join(' ')} L ${graphWidth},${graphHeight} Z` + const areaPath = `M ${startX},${graphHeight} ${pathPoints.map(p => `L ${p.x},${p.y}`).join(' ')} L ${graphWidth},${graphHeight} Z` // Calculate current vested position let currentY = graphHeight @@ -130,10 +143,8 @@ export const VestingGraph = ({ globalLock, className = "" }: VestingGraphProps) } } - const timeStartToCliff = Math.max(0, cliffTime - startTime) - const timeCliffToEnd = Math.max(0, endTime - cliffTime) - const timeNowToCliff = Math.max(0, cliffTime - now) - const timeNowToEnd = Math.max(0, endTime - now) + const timeNowToStart = Math.max(0, startTime - now) + const lockDuration = Math.max(0, endTime - startTime) return { width, @@ -149,18 +160,36 @@ export const VestingGraph = ({ globalLock, className = "" }: VestingGraphProps) startTime, cliffTime, endTime, - timeStartToCliff, - timeCliffToEnd, - timeNowToCliff, - timeNowToEnd, + startX, + cliffX, + timeNowToStart, + lockDuration, formatTimeRemaining, startEqualsCliff, - cliffEqualsEnd + cliffEqualsEnd, + nowBeforeStart: now < startTime } - }, [globalLock, vestingData]) + }, [globalLock, vestingData, blockTimestamp]) return (
+ {/* TGE Notice for Auction ATP */} + {isAuctionATP && ( +
+
+ TGE Notice: Tokens become available at TGE. TGE is decided by governance. Earliest anticipated in 90 days from start date. Latest is{' '} + + {new Date(Number(globalLock.endTime) * 1000).toLocaleDateString('en-US', { + day: 'numeric', + month: 'short', + year: 'numeric' + })} + + {' '}as shown in the graph below. +
+
+ )} + - {/* Show cliff labels on outer (left) side if ratio <= 0.6, inside graph if ratio > 0.6 */} - {vestingData.cliffUnlockRatio > 0.6 ? ( - <> - - Cliff Unlock - - - {formatTokenAmount(BigInt(Math.floor(Number(globalLock.allocation) * vestingData.cliffUnlockRatio)), decimals, symbol)} - - - {(vestingData.cliffUnlockRatio * 100).toFixed(2)}% - - - ) : ( - <> - - Cliff Unlock - - - {formatTokenAmount(BigInt(Math.floor(Number(globalLock.allocation) * vestingData.cliffUnlockRatio)), decimals, symbol)} - - - {(vestingData.cliffUnlockRatio * 100).toFixed(2)}% - - + {/* Show cliff labels only when cliff unlock ratio > 0 (to avoid overlapping with 0% labels) */} + {/* Position on outer (left) side if ratio <= 0.6, inside graph if ratio > 0.6 */} + {vestingData.cliffUnlockRatio > 0.01 && ( + vestingData.cliffUnlockRatio > 0.6 ? ( + <> + + Cliff Unlock + + + {formatTokenAmount(BigInt(Math.floor(Number(globalLock.allocation) * vestingData.cliffUnlockRatio)), decimals, symbol)} + + + {(vestingData.cliffUnlockRatio * 100).toFixed(2)}% + + + ) : ( + <> + + Cliff Unlock + + + {formatTokenAmount(BigInt(Math.floor(Number(globalLock.allocation) * vestingData.cliffUnlockRatio)), decimals, symbol)} + + + {(vestingData.cliffUnlockRatio * 100).toFixed(2)}% + + + ) )} - - 0 {symbol} - - - 0% - {/* Vesting area */} + {/* Horizontal line to Y-axis showing vested amount */} + {graphData.currentY < graphData.graphHeight && ( + <> + + {/* Vested amount label on Y-axis */} + + {formatTokenAmount(vestingData.currentVestedAmount, decimals, symbol)} + + + )} + {/* Current position dot */} - {/* Time remaining from NOW */} - - {/* NOW → Cliff */} - {graphData.timeNowToCliff > 0 && ( - - {graphData.formatTimeRemaining(graphData.timeNowToCliff)} → Cliff - - )} - - {/* NOW → End */} - {graphData.timeNowToEnd > 0 && ( - 0 ? "-15" : "-30"} - textAnchor="middle" - fontSize="10" - fill="#FF2DF4" - className="font-mono font-bold" - > - {graphData.formatTimeRemaining(graphData.timeNowToEnd)} → End - - )} - )} - {/* Time period labels - Always shown */} - {/* Start to Cliff duration */} - - - {graphData.formatTimeRemaining(graphData.timeStartToCliff)} - - - (Start → Cliff) - - + {/* Time period labels */} + {/* Now to Start - only show when now is before start */} + {graphData.nowBeforeStart && ( + + + {graphData.formatTimeRemaining(graphData.timeNowToStart)} + + + (Until Vesting Starts) + + + )} - {/* Cliff to End duration */} + {/* Lock Duration (Start to End) */} - {graphData.formatTimeRemaining(graphData.timeCliffToEnd)} + {graphData.formatTimeRemaining(graphData.lockDuration)} - (Cliff → End) + (Vesting Duration) - {/* Cliff unlock indicator */} - - - - {Math.round(vestingData.cliffUnlockRatio * 100)}% - - + {/* Cliff unlock indicator - only show when ratio > 0 */} + {vestingData.cliffUnlockRatio > 0.01 && ( + + + + {Math.round(vestingData.cliffUnlockRatio * 100)}% + + + )} {/* Date markers with vertical lines - Handle collisions */} {!graphData.startEqualsCliff && ( - Note: After cliff, tokens unlock linearly per Ethereum block. + Note: At the Unlock Start Time, tokens unlock linearly per Ethereum block.
) diff --git a/staking-dashboard/src/hooks/atp/useVestingCalculation.ts b/staking-dashboard/src/hooks/atp/useVestingCalculation.ts index 5f28f0dff..80b28d2ff 100644 --- a/staking-dashboard/src/hooks/atp/useVestingCalculation.ts +++ b/staking-dashboard/src/hooks/atp/useVestingCalculation.ts @@ -27,12 +27,16 @@ interface VestingCalculation { /** * Calculate vesting timeline and amounts based on globalLock parameters + * @param globalLock - Vesting schedule parameters + * @param blockTimestamp - Optional blockchain timestamp to use instead of Date.now() */ export function useVestingCalculation( - globalLock: GlobalLock + globalLock: GlobalLock, + blockTimestamp?: bigint ): VestingCalculation { return useMemo(() => { - const now = getCurrentEpochSeconds() + // Use blockchain timestamp if provided, otherwise fall back to system time + const now = blockTimestamp ? Number(blockTimestamp) : getCurrentEpochSeconds() const timePoints: VestingTimePoint[] = [] const totalAmount = globalLock.allocation const vestingStartTime = Number(globalLock.startTime) @@ -111,5 +115,5 @@ export function useVestingCalculation( timeRemaining, currentVestedAmount } - }, [globalLock]) + }, [globalLock, blockTimestamp]) } \ No newline at end of file diff --git a/staking-dashboard/src/utils/atpFormatters.ts b/staking-dashboard/src/utils/atpFormatters.ts index 4cf4b4b5a..1bed60522 100644 --- a/staking-dashboard/src/utils/atpFormatters.ts +++ b/staking-dashboard/src/utils/atpFormatters.ts @@ -45,10 +45,15 @@ export function getStakeableAmount(atp: ATPData): bigint { /** * Calculate time until claimable + * @param atp - ATP data with globalLock info + * @param blockTimestamp - Optional blockchain timestamp to use instead of Date.now() + * This ensures consistency with anvil time warps during testing */ -export function getTimeToClaimForATP(atp: ATPData): string { +export function getTimeToClaimForATP(atp: ATPData, blockTimestamp?: bigint): string { if (atp.globalLock?.endTime) { - const now = Math.floor(Date.now() / 1000) + const now = blockTimestamp + ? Number(blockTimestamp) + : Math.floor(Date.now() / 1000) const endTime = Number(atp.globalLock.endTime) const timeLeft = endTime - now diff --git a/staking-dashboard/src/utils/factoryHelpers.ts b/staking-dashboard/src/utils/factoryHelpers.ts deleted file mode 100644 index aea9648c8..000000000 --- a/staking-dashboard/src/utils/factoryHelpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Address } from "viem"; - -/** - * Map of factory addresses to human-readable names - * These are network-specific addresses - */ -export const FACTORY_NAMES: Record = { - // Mainnet factories - "0xaa292e8611adf267e563f334ee42320ac96d0463": "Genesis Sale", - "0x3155755b79aa083bd953911c92705b7aa82a18f9": "Auction", - "0xa17ea96757c9bb9b41a12ef5073c51129937ffae": "Employee", - "0x278f39b11b3de0796561e85cb48535c9f45ddfcc": "Investor", - - // Anvil/Dev factories - "0xd6e1afe5ca8d00a2efc01b89997abe2de47fdfaf": "Employee", - "0x6f6f570f45833e249e27022648a26f4076f48f78": "Investor", -}; - -/** - * Get human-readable factory name from factory address - * Falls back to "Unknown" if factory address is not recognized - */ -export function getFactoryName(factoryAddress?: Address | string): string { - if (!factoryAddress) { - return "Unknown"; - } - - const normalized = factoryAddress.toLowerCase(); - return FACTORY_NAMES[normalized] || "Unknown"; -} - -/** - * Get short factory identifier (first word only) - * Used for compact displays - */ -export function getFactoryShortName(factoryAddress?: Address | string): string { - const fullName = getFactoryName(factoryAddress); - return fullName.split(" ")[0]; // Returns "Genesis", "Auction", "Employee", "Investor", or "Unknown" -} From 2ea22a7eda8f67014834821a1e74aa848ebfd364 Mon Sep 17 00:00:00 2001 From: Amin Sammara Date: Wed, 25 Feb 2026 23:31:44 -0500 Subject: [PATCH 3/4] chore: bump indexer schema versions for MATP/LATP factory changes --- db-schemas.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-schemas.json b/db-schemas.json index 30bfcbc66..8e02edf95 100644 --- a/db-schemas.json +++ b/db-schemas.json @@ -1,6 +1,6 @@ { "atp-indexer": { "testnet": "atp-indexer-testnet-v03", - "prod": "atp-indexer-prod-v15" + "prod": "atp-indexer-prod-v16" } } From 90d584d9a69a9b1c63745d7ead14e5ec5abc51c8 Mon Sep 17 00:00:00 2001 From: Amin Sammara Date: Thu, 26 Feb 2026 02:33:17 -0500 Subject: [PATCH 4/4] fix: display milestone IDs as 1-indexed in UI --- staking-dashboard/src/components/ATPCard/ATPCard.tsx | 2 +- .../src/components/ATPStakingCard/ATPStakingCard.tsx | 2 +- .../src/components/SetOperatorModal/SetOperatorModal.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/staking-dashboard/src/components/ATPCard/ATPCard.tsx b/staking-dashboard/src/components/ATPCard/ATPCard.tsx index 9ddef17f7..9b6eafdef 100644 --- a/staking-dashboard/src/components/ATPCard/ATPCard.tsx +++ b/staking-dashboard/src/components/ATPCard/ATPCard.tsx @@ -152,7 +152,7 @@ export default function ATPCard({
{atp.milestoneId !== undefined && ( - Milestone {atp.milestoneId.toString()} + Milestone {Number(atp.milestoneId) + 1} )} {data.milestoneId !== undefined && ( - Milestone {Number(data.milestoneId)} + Milestone {Number(data.milestoneId) + 1} )} diff --git a/staking-dashboard/src/components/SetOperatorModal/SetOperatorModal.tsx b/staking-dashboard/src/components/SetOperatorModal/SetOperatorModal.tsx index e26a3d3e8..e25fa54be 100644 --- a/staking-dashboard/src/components/SetOperatorModal/SetOperatorModal.tsx +++ b/staking-dashboard/src/components/SetOperatorModal/SetOperatorModal.tsx @@ -105,7 +105,7 @@ export default function SetOperatorModal({ {getTypeName(atp)} {atp.milestoneId !== undefined && ( - Milestone {atp.milestoneId.toString()} + Milestone {Number(atp.milestoneId) + 1} )}