+
+ Stake creator keys
+
+
+ Lock your keys for a period to earn staking rewards from the
+ reward pool.
+
+
+ {/* Key selector */}
+
+
+
+
+
+
+
+
+ {/* Quantity input */}
+
+
+
+ setQuantity(e.target.value)}
+ placeholder={
+ maxQuantity > 0
+ ? `Max ${maxQuantity}`
+ : 'Select a key first'
+ }
+ className="w-full rounded-xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white placeholder:text-white/25 transition-colors focus:border-amber-500/40 focus:outline-none"
+ disabled={!selectedKey}
+ />
+ {maxQuantity > 0 && (
+
+ )}
+
+
+
+ {/* Lock period selection */}
+
+
+ Lock period
+
+
+ {LOCK_PERIOD_OPTIONS.map((option, idx) => (
+
+ ))}
+
+
+
+ {/* Estimated rewards preview */}
+ {isValid && lockOption && (
+
+
+ Estimated rewards
+
+
+ {(
+ (parsedQuantity *
+ lockOption.apyBps *
+ lockOption.days) /
+ (365 * 10000)
+ ).toFixed(4)}{' '}
+ XLM
+
+
+ Based on current pool rate. Actual rewards vary.
+
+
+ )}
+
+ {/* Submit */}
+
+
+ );
+}
+
+// ── Active positions table row ──────────────────────────────────────────
+
+function PositionRow({
+ position,
+ address,
+}: {
+ position: StakingPosition;
+ address: string;
+}) {
+ const [nowMs, setNowMs] = useState(() => Date.now());
+ const unstakeMutation = useUnstakeKeysMutation(address);
+ const claimMutation = useClaimStakingRewardsMutation(address);
+
+ useEffect(() => {
+ const id = window.setInterval(() => setNowMs(Date.now()), 1000);
+ return () => window.clearInterval(id);
+ }, []);
+
+ const unlocked = isStakePositionUnlocked(position, nowMs);
+ const remainingSec = computeRemainingStakeLockSeconds(position, nowMs);
+ const claimable = position.claimableReward ?? 0;
+
+ const handleClaim = () => {
+ claimMutation.mutate({
+ positionId: position.id,
+ keyId: position.keyId,
+ });
+ };
+
+ const handleUnstake = () => {
+ unstakeMutation.mutate({
+ positionId: position.id,
+ keyId: position.keyId,
+ });
+ };
+
+ const expiryDate = new Date(position.unlockLedger * 1000);
+
+ return (
+
+ {/* Key info */}
+
+
+ {position.keyName}
+
+
+ Staked {formatStakedQuantity(position.stakedQuantity)}
+
+
+
+ {/* Expiry */}
+
+
+ Lock expiry
+
+
+ {expiryDate.toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+
+
+ {/* Lock status */}
+
+ {unlocked ? (
+
+
+ Unlocked
+
+ ) : (
+
+
+ {formatCountdownTime(remainingSec)}
+
+ )}
+
+
+ {/* Claimable rewards */}
+
+
+ Claimable
+
+
+ {formatClaimableReward(claimable)}
+
+
+
+ {/* Actions */}
+
+ {claimable > 0 && (
+
+ )}
+
+
+
+ );
+}
+
+// ── Skeleton rows ───────────────────────────────────────────────────────
+
+function PositionRowSkeleton() {
+ return (
+