Skip to content
Merged
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: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ When touching transaction and position flows, validation MUST include all releva
15. **Bundler3 swap route integrity**: Bundler3 swap leverage/deleverage must use adapter flashloan callbacks (not pre-swap borrow gating), with `callbackHash`/`reenter` wiring and adapter token flows matching on-chain contracts; before submit, verify aggregator quote/tx parity (trusted target, exact/min calldata offsets, and same-pair combined-sell normalization) so previewed borrow/repay/collateral amounts cannot drift from executed inputs; prefer exact-in close executors that fully consume the withdrawn collateral over max-sell refund paths that can strand shared-adapter balances, and only relax build-time allowance checks for adapter-executed paths when the failure is allowance-specific.
16. **Quote, preview, and route-state integrity**: when a preview depends on one or more aggregator legs, surface failures from every required leg and use conservative fallbacks (`0`, disable submit) instead of optimistic defaults, but optional exact-close quote legs must not block still-valid partial execution paths; if a close-out path depends on a dedicated debt-close bound (for example BUY/max-close quoting) plus a separate execution preview, full-close / repay-by-shares intent must be driven by one explicit close-route flag shared by preview and tx building, the close executor must be satisfiable under the same slippage floor shown in UI, and if the current sell quote can fully close debt while the exact close bound is still unresolved the UI must fail closed rather than silently degrading to a dust-leaving partial path; for exact-in swap deleverage routes, the exact close bound is a threshold for switching into close mode, not a universal input cap, so valid oversell/refund paths must remain available and previews must continue to match the selected exact-in amount; preview rate/slippage must come from the executable quote/config, selected route mode must never execute a different route while capability probes are in-flight, and route controls/entry CTAs must stay consistent with capability probes without duplicate low-signal UI.
17. **Permit2 time-units and adapter balance hygiene**: Permit2 `expiration`/`sigDeadline` values must always be unix seconds (never milliseconds), and every adapter-executed swap leg must sweep leftover source tokens from the adapter before bundle exit so shared-adapter balances cannot accumulate between transactions.
18. **Smart-rebalance constraint integrity**: treat user max-allocation limits (especially `0%`) as hard constraints in planner output and previews, not soft objective hints; when liquidity/capacity permits, planner targets must not leave avoidable residual allocation above cap, and full-exit targets must use tx-construction-compatible withdrawal semantics so previewed and executed allocations stay aligned.
19. **Monotonic transaction-step updates**: never call `tracking.update(...)` unconditionally in tx hooks; compute step order for the active flow and only advance when the target step is strictly later than the current runtime step, so auth/permit pre-satisfied states cannot regress the stepper backwards.
20. **Share-based full-exit withdrawals**: when a rebalance target leaves only dust in a source market, tx builders must switch to share-based `morphoWithdraw` (full shares burn with expected-assets guard) instead of asset-amount withdraws, so "empty market" intent cannot strand residual dust due rounding.

### REQUIRED: Regression Rule Capture

Expand Down
32 changes: 31 additions & 1 deletion src/components/common/ProcessModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import { Modal, ModalBody } from '@/components/common/Modal';
import { ProcessStepList } from '@/components/common/ProcessStepList';
import type { ActiveTransaction } from '@/stores/useTransactionProcessStore';
import type { ActiveTransaction, TransactionSummaryItem } from '@/stores/useTransactionProcessStore';

type ProcessModalProps = {
/**
Expand Down Expand Up @@ -50,6 +50,33 @@ type ProcessModalProps = {
* );
* ```
*/
function SummaryBlock({ items }: { items: TransactionSummaryItem[] }) {
return (
<div className="flex flex-col gap-1.5 rounded-lg bg-surface p-3">
{items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between text-sm"
Comment on lines +56 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a collision-safe key for summary rows.

Line 58 uses item.label as the key. Repeated labels can collide and cause row reuse glitches.

Suggested fix
-      {items.map((item) => (
+      {items.map((item, index) => (
         <div
-          key={item.label}
+          key={`${item.label}-${item.value}-${index}`}
           className="flex items-center justify-between text-sm"
         >

As per coding guidelines: "Use the key prop for elements in iterables (prefer unique IDs over array indices)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/ProcessModal.tsx` around lines 56 - 59, The summary row
list in ProcessModal.tsx uses item.label as the React key inside items.map,
which can collide for duplicate labels; change the key to a collision-safe
unique identifier (e.g., an id property on the item or a generated stable id) by
updating the items.map callback to use item.id (or another guaranteed-unique
field) instead of item.label so each rendered <div> has a stable, unique key to
prevent row reuse glitches.

>
<span className="text-secondary">{item.label}</span>
<span className="flex items-center gap-1.5 font-medium">
<span>{item.value}</span>
{item.detail && (
<span
className={
item.detailColor === 'positive' ? 'text-green-600' : item.detailColor === 'negative' ? 'text-red-500' : 'text-secondary'
}
>
{item.detail}
</span>
)}
</span>
</div>
))}
</div>
);
}

export function ProcessModal({ transaction, onDismiss, title, description, children }: ProcessModalProps) {
// Don't render if no transaction or modal is hidden
if (!transaction?.isModalVisible) return null;
Expand Down Expand Up @@ -79,6 +106,9 @@ export function ProcessModal({ transaction, onDismiss, title, description, child
</button>
</div>
<ModalBody className="gap-5">
{transaction.metadata.summaryItems && transaction.metadata.summaryItems.length > 0 && (
<SummaryBlock items={transaction.metadata.summaryItems} />
)}
{children}
<ProcessStepList
steps={transaction.steps}
Expand Down
18 changes: 18 additions & 0 deletions src/config/smart-rebalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type Address, isAddress } from 'viem';

/**
* Frontend-configured fee recipient for Smart Rebalance.
*
* Set `NEXT_PUBLIC_SMART_REBALANCE_FEE_RECIPIENT` in your env to override.
* Fallback keeps current production behavior until explicitly changed.
*/
const DEFAULT_FEE_RECIPIENT = '0xc8440DF82b5Eb7Ff1dc1DcB4d756bd35B9340B7C';

const configuredRecipient = process.env.NEXT_PUBLIC_SMART_REBALANCE_FEE_RECIPIENT?.trim();
const resolvedRecipient = configuredRecipient ?? DEFAULT_FEE_RECIPIENT;

if (!isAddress(resolvedRecipient)) {
throw new Error('NEXT_PUBLIC_SMART_REBALANCE_FEE_RECIPIENT must be a valid EVM address.');
}

export const SMART_REBALANCE_FEE_RECIPIENT: Address = resolvedRecipient;
34 changes: 19 additions & 15 deletions src/features/positions/components/allocation-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,32 @@ import { MONARCH_PRIMARY } from '@/constants/chartColors';

type AllocationCellProps = {
amount: number;
symbol: string;
symbol?: string;
percentage: number;
compact?: boolean;
};

/**
* Combined allocation display component showing percentage as a circular indicator
* alongside the amount. Used in expanded position tables for consistent allocation display.
*/
export function AllocationCell({ amount, symbol, percentage }: AllocationCellProps) {
export function AllocationCell({ amount, symbol, percentage, compact = false }: AllocationCellProps) {
const isZero = amount === 0;
const displayPercentage = Math.min(percentage, 100); // Cap at 100% for display

// Calculate SVG circle properties for progress indicator
const radius = 8;
const radius = compact ? 6 : 8;
const iconSize = compact ? 16 : 20;
const strokeWidth = compact ? 2 : 3;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (displayPercentage / 100) * circumference;

return (
<div className="flex items-center justify-end gap-2">
<div className={`flex items-center justify-end ${compact ? 'gap-1' : 'gap-2'}`}>
{/* Amount and symbol */}
<span className={isZero ? 'text-secondary' : ''}>
{isZero ? '0' : formatReadable(amount)} {symbol}
<span className={`${compact ? 'text-xs' : ''} ${isZero ? 'text-secondary' : ''}`}>
{isZero ? '0' : formatReadable(amount)}
{symbol ? ` ${symbol}` : ''}
</span>

{/* Circular percentage indicator */}
Expand All @@ -40,29 +44,29 @@ export function AllocationCell({ amount, symbol, percentage }: AllocationCellPro
>
<div className="flex-shrink-0">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
width={iconSize}
height={iconSize}
viewBox={`0 0 ${iconSize} ${iconSize}`}
className="transform -rotate-90"
>
{/* Background circle */}
<circle
cx="10"
cy="10"
cx={iconSize / 2}
cy={iconSize / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeWidth={strokeWidth}
className="text-gray-200 dark:text-gray-700"
/>
{/* Progress circle */}
<circle
cx="10"
cy="10"
cx={iconSize / 2}
cy={iconSize / 2}
r={radius}
fill="none"
stroke={isZero ? 'currentColor' : MONARCH_PRIMARY}
strokeWidth="3"
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
className={isZero ? 'text-gray-300 dark:text-gray-600' : ''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';

type PositionActionsDropdownProps = {
isOwner: boolean;
isOwner?: boolean;
onRebalanceClick: () => void;
};

Expand All @@ -17,7 +17,6 @@ export function PositionActionsDropdown({ isOwner, onRebalanceClick }: PositionA
};

const handleKeyDown = (e: React.KeyboardEvent) => {
// Stop propagation on keyboard events too
e.stopPropagation();
};

Expand All @@ -43,7 +42,7 @@ export function PositionActionsDropdown({ isOwner, onRebalanceClick }: PositionA
onClick={onRebalanceClick}
startContent={<TbArrowsRightLeft className="h-4 w-4" />}
disabled={!isOwner}
className={isOwner ? '' : 'opacity-50 cursor-not-allowed'}
className={isOwner ? '' : 'cursor-not-allowed opacity-50'}
>
Rebalance
</DropdownMenuItem>
Expand Down
Loading