diff --git a/.gitignore b/.gitignore
index 23407396ef9..b4d14549b04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,4 @@ yarn-error.log*
# translation benchmark data
scripts/translations/benchmark/
+.gemini/
diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml
index 29409da8389..0da1ee74806 100644
--- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml
+++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml
@@ -1,32 +1,101 @@
name: Chainflip Lending Revamp UI
-description: Validates the revamped Chainflip lending surfaces for USDC pool and key action modals.
+description: Validates the revamped Chainflip lending dashboard, init view, funded view with sections, and key action modals.
route: /chainflip-lending
steps:
- - name: Chainflip lending dashboard
- instruction: Open the chainflip lending dashboard and ensure the refreshed layout is visible.
- expected: Chainflip lending dashboard cards and market tables are visible.
+ # === No-wallet state ===
+ - name: Landing page without wallet
+ instruction: Open the chainflip lending page without a wallet connected.
+ expected: >
+ Header shows Total Supplied, Available Liquidity, Total Borrowed stat cards.
+ "All Markets" section with "Lending Markets" heading, description text, and 6-column table
+ (Pool, Supply APY, Total Supplied, Borrow APR, Total Borrowed, Utilisation).
+ USDC row shows non-zero APY. Connect Wallet button is visible.
screenshot: true
- - name: Open usdc pool
- instruction: Navigate into the USDC pool from the All Markets table.
- expected: USDC pool page is visible with action tabs.
+
+ # === Init view (fresh wallet, no positions) ===
+ - name: Init view for fresh wallet
+ instruction: Connect a wallet with no chainflip lending positions. Observe the My Dashboard tab.
+ expected: >
+ Hero card with "Get Started" badge, "Deposit your first asset to get started" heading,
+ blue "+ Deposit" button, "Requires 2 FLIP" note, and asset constellation art (ETH, BTC, USDC, USDT, SOL icons with orbital rings).
+ Lending Markets table with 6 columns below.
+ Two info cards at bottom: "Earn Yield" (green sparkle art) and "Borrow Against Collateral" (purple refresh art).
screenshot: true
- - name: Supply modal
- instruction: Open Supply action and verify supply input controls.
- expected: Supply modal is open with amount input, max and submit controls.
+
+ # === Funded dashboard ===
+ - name: Funded dashboard overview
+ instruction: Connect a wallet with free balance on chainflip (e.g. import TorSwapKeyStore4). Navigate to My Dashboard tab.
+ expected: >
+ Header stat cards: Free Balance (with value), Supplied $0.00, Collateral $0.00, Borrowed $0.00.
+ My Dashboard / Markets tabs visible, My Dashboard selected.
+ Free Balance section with Asset/Balance columns, listing assets with balances.
+ Blue filled "+ Deposit" button and ghost "Withdraw" button in Free Balance header.
+ Next Steps sidebar card with green sparkle art, "Supply" and "Add Collateral" buttons.
screenshot: true
- - name: Deposit and egress modal
- instruction: Open Deposit to Chainflip tab and open Withdraw modal.
- expected: Egress modal is open with destination toggle and amount controls.
+
+ - name: Empty sections display
+ instruction: Scroll down on the funded dashboard to see Supplied, Collateral, and Borrowed sections.
+ expected: >
+ Supplied section: "$0.00", "No earning positions yet" empty state with blue "+ Deposit" button.
+ Collateral section: "$0.00", "Provide collateral to start borrowing" with blue "+ Add Collateral" button.
+ Borrowed section: "$0.00", "No Active Loans" with blue "+ Add Collateral" button.
screenshot: true
- - name: Collateral modal ltv gauge
- instruction: Open Collateral tab and open Add Collateral modal.
- expected: LTV gauge shows target, soft liquidation and hard liquidation labels without overlap.
+
+ # === Action modals from dashboard ===
+ - name: Deposit modal from dashboard
+ instruction: Click the blue "+ Deposit" button in the Free Balance section header.
+ expected: >
+ Deposit modal opens. Asset selector card with icon, name, balance visible.
+ Amount input field with asset icon. Info rows below divider.
+ Blue "Next" button at bottom.
screenshot: true
- - name: Borrow modal
- instruction: Open Manage Loan tab and open Borrow modal.
- expected: Borrow modal is open with amount input and target LTV section.
+
+ - name: Supply modal from Next Steps
+ instruction: Close deposit modal. Click "Supply" button in the Next Steps sidebar card.
+ expected: >
+ Supply modal opens with "Asset to supply" selector card.
+ Pool APY and Current Position stat boxes side-by-side.
+ Amount input with asset icon. Info rows: Estimated yearly earnings, Pool Share, Auto-compounding, Risk band.
+ Blue "Next" button at bottom.
screenshot: true
- - name: Repay modal
- instruction: Open Repay modal from Manage Loan tab.
- expected: Repay modal is open with full repayment toggle and amount controls.
+
+ - name: Add Collateral modal
+ instruction: Close supply modal. Click "+ Add Collateral" button from the Collateral section empty state.
+ expected: >
+ Add Collateral modal opens with asset selection, amount input, and LTV gauge preview.
+ screenshot: true
+
+ # === Loan Health / LTV bar ===
+ - name: Loan Health bar (requires active loan)
+ instruction: >
+ If the wallet has an active loan (collateral posted + borrowed amount > 0),
+ verify the Loan Health card appears above the section cards on My Dashboard.
+ It should show: heart rate monitor icon, "Loan Health" label, Current LTV percentage
+ (color-coded green/yellow/red), Liquidation Distance fiat value on the right,
+ and a horizontal multi-segment bar (Safe green / Risky yellow / Liquidation red)
+ with a skull icon at the hard liquidation boundary.
+ If no active loan exists, the Loan Health card should NOT render (this is correct).
+ expected: >
+ Loan Health card renders when loans exist with LTV gauge bar,
+ or correctly hidden when no loans.
+ data-testid: chainflip-lending-loan-health
+ screenshot: true
+
+ # === Visual polish checks ===
+ - name: Empty section dashed borders
+ instruction: >
+ On the funded dashboard with no supply/collateral/borrowed positions,
+ verify that the Supplied, Collateral, and Borrowed section cards have
+ dashed borders (not solid) to visually indicate empty state.
+ expected: >
+ Empty section cards have dashed border style. Free Balance card (with data) has solid border.
+ screenshot: true
+
+ # === Markets tab ===
+ - name: Markets tab
+ instruction: Close any open modal. Switch to the "Markets" tab.
+ expected: >
+ Lending Markets heading with description.
+ 6-column table (Pool, Supply APY, Total Supplied, Borrow APR, Total Borrowed, Utilisation).
+ USDC and Tether rows show non-zero APY and utilisation.
screenshot: true
diff --git a/src/assets/chainflip-lending/borrow-glow.svg b/src/assets/chainflip-lending/borrow-glow.svg
new file mode 100644
index 00000000000..6c76440904b
--- /dev/null
+++ b/src/assets/chainflip-lending/borrow-glow.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/chainflip-lending/borrow-ring-1.svg b/src/assets/chainflip-lending/borrow-ring-1.svg
new file mode 100644
index 00000000000..a92b59e8483
--- /dev/null
+++ b/src/assets/chainflip-lending/borrow-ring-1.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/borrow-ring-2.svg b/src/assets/chainflip-lending/borrow-ring-2.svg
new file mode 100644
index 00000000000..d1d3ce292b5
--- /dev/null
+++ b/src/assets/chainflip-lending/borrow-ring-2.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/borrow-ring-3.svg b/src/assets/chainflip-lending/borrow-ring-3.svg
new file mode 100644
index 00000000000..9548e3cdd19
--- /dev/null
+++ b/src/assets/chainflip-lending/borrow-ring-3.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/borrow-ring-inner.svg b/src/assets/chainflip-lending/borrow-ring-inner.svg
new file mode 100644
index 00000000000..4fcf8325f28
--- /dev/null
+++ b/src/assets/chainflip-lending/borrow-ring-inner.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/earn-glow.svg b/src/assets/chainflip-lending/earn-glow.svg
new file mode 100644
index 00000000000..dd85a9c9cb6
--- /dev/null
+++ b/src/assets/chainflip-lending/earn-glow.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/chainflip-lending/earn-ring-inner.svg b/src/assets/chainflip-lending/earn-ring-inner.svg
new file mode 100644
index 00000000000..522748941c5
--- /dev/null
+++ b/src/assets/chainflip-lending/earn-ring-inner.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/earn-ring-middle.svg b/src/assets/chainflip-lending/earn-ring-middle.svg
new file mode 100644
index 00000000000..13ca13d1cde
--- /dev/null
+++ b/src/assets/chainflip-lending/earn-ring-middle.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/earn-ring-outer.svg b/src/assets/chainflip-lending/earn-ring-outer.svg
new file mode 100644
index 00000000000..e7615115bd3
--- /dev/null
+++ b/src/assets/chainflip-lending/earn-ring-outer.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/glow-btc.svg b/src/assets/chainflip-lending/glow-btc.svg
new file mode 100644
index 00000000000..9aa0db74b36
Binary files /dev/null and b/src/assets/chainflip-lending/glow-btc.svg differ
diff --git a/src/assets/chainflip-lending/glow-eth.svg b/src/assets/chainflip-lending/glow-eth.svg
new file mode 100644
index 00000000000..2a33fe95f74
Binary files /dev/null and b/src/assets/chainflip-lending/glow-eth.svg differ
diff --git a/src/assets/chainflip-lending/orbital-btc.svg b/src/assets/chainflip-lending/orbital-btc.svg
new file mode 100644
index 00000000000..d9e469a2789
--- /dev/null
+++ b/src/assets/chainflip-lending/orbital-btc.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/orbital-eth.svg b/src/assets/chainflip-lending/orbital-eth.svg
new file mode 100644
index 00000000000..73ae0542394
--- /dev/null
+++ b/src/assets/chainflip-lending/orbital-eth.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/orbital-sol.svg b/src/assets/chainflip-lending/orbital-sol.svg
new file mode 100644
index 00000000000..1ad6262f3ba
--- /dev/null
+++ b/src/assets/chainflip-lending/orbital-sol.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/orbital-tether.svg b/src/assets/chainflip-lending/orbital-tether.svg
new file mode 100644
index 00000000000..528ecd9a152
--- /dev/null
+++ b/src/assets/chainflip-lending/orbital-tether.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/orbital-usdc.svg b/src/assets/chainflip-lending/orbital-usdc.svg
new file mode 100644
index 00000000000..db36016ed6c
--- /dev/null
+++ b/src/assets/chainflip-lending/orbital-usdc.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/chainflip-lending/refresh-icon.svg b/src/assets/chainflip-lending/refresh-icon.svg
new file mode 100644
index 00000000000..0679a629a48
--- /dev/null
+++ b/src/assets/chainflip-lending/refresh-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/chainflip-lending/sparkles-icon.svg b/src/assets/chainflip-lending/sparkles-icon.svg
new file mode 100644
index 00000000000..8d12cd182b5
--- /dev/null
+++ b/src/assets/chainflip-lending/sparkles-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json
index 45bd0ec9ad3..40cef1a3edd 100644
--- a/src/assets/translations/en/main.json
+++ b/src/assets/translations/en/main.json
@@ -2456,6 +2456,8 @@
"manageLoan": "Manage Loan",
"supply": {
"title": "Supply",
+ "subtitle": "Allocate your free balance into lending pools to earn yield",
+ "asset": "Asset",
"amount": "Amount",
"available": "Available to supply",
"availableTooltip": "Your free balance on Chainflip State Chain available for lending",
@@ -2463,6 +2465,14 @@
"confirmTitle": "Confirm Supply",
"confirmDescription": "Supply %{amount} %{asset} to the lending pool",
"confirmAndSupply": "Confirm & Supply",
+ "poolApy": "Pool APY",
+ "currentPosition": "Current Position",
+ "autoCompounding": "Auto-compounding",
+ "enabled": "Enabled",
+ "destination": "Destination",
+ "lendingPool": "%{asset} Lending Pool",
+ "yearlyEarningsSuffix": "%{amount} / year",
+ "year": "year",
"executingTitle": "Supplying...",
"executingDescription": "Your supply is being processed",
"successTitle": "Supply Successful",
@@ -2475,10 +2485,16 @@
"signing": "Signing supply transaction",
"confirming": "Confirming supply"
},
- "noFreeBalance": "No free balance. Deposit to Chainflip first."
+ "noFreeBalance": "No free balance. Deposit to Chainflip first.",
+ "assetToSupply": "Asset to supply",
+ "poolShare": "Pool Share",
+ "riskBand": "Risk band",
+ "conservativeStablecoin": "Conservative stablecoin pool",
+ "volatileAsset": "Volatile asset pool"
},
"borrow": {
"title": "Borrow",
+ "subtitle": "Borrow assets against your collateral",
"amount": "Amount",
"available": "Available to borrow",
"maxLtv": "Max LTV (80%)",
@@ -2487,6 +2503,7 @@
"confirmTitle": "Confirm Borrow",
"confirmDescription": "Borrow %{amount} %{asset} against your collateral",
"confirmAndBorrow": "Confirm & Borrow",
+ "asset": "Asset",
"executingTitle": "Borrowing...",
"executingDescription": "Your loan is being processed",
"successTitle": "Borrow Successful",
@@ -2529,6 +2546,8 @@
},
"supplyApy": "Supply APY",
"supplyApyTooltip": "Annual percentage yield earned by supplying assets to this pool.",
+ "borrowApr": "Borrow APR",
+ "borrowAprTooltip": "Annual percentage rate charged for borrowing from this pool.",
"borrowRate": "Borrow Rate",
"utilisation": "Utilisation",
"utilisationTooltip": "Percentage of supplied assets currently being borrowed. Higher utilisation means higher yields but less available liquidity.",
@@ -2543,6 +2562,8 @@
"add": "Add Collateral",
"remove": "Remove Collateral",
"amount": "Amount",
+ "asset": "Asset",
+ "action": "Action",
"availableToAdd": "Available to add",
"availableToRemove": "Available to remove",
"confirmAddTitle": "Confirm Add Collateral",
@@ -2582,9 +2603,12 @@
"connectWalletDescription": "Connect your wallet to view your lending positions.",
"protocolStats": "Protocol Stats",
"deposit": {
+ "subtitle": "Move assets from your wallet into your free balance",
"amount": "Amount",
"available": "Available",
"explainer": "Assets will be deposited to your Chainflip State Chain account. Once deposited, you can supply to lending pools to earn yield.",
+ "asset": "Asset",
+ "recoveryAddress": "Recovery Address",
"openChannel": "Open Deposit Channel",
"confirmTitle": "Confirm Deposit",
"confirmDescription": "Deposit %{amount} %{asset} to your Chainflip State Chain account. You will be guided through each required step and prompted to sign transactions as needed.",
@@ -2632,8 +2656,10 @@
"myBalancesTitle": "Chainflip Lending - My Balances",
"withdraw": {
"title": "Withdraw",
+ "subtitle": "Withdraw assets from the lending pool back to your free balance",
"amount": "Amount",
"available": "Available to withdraw",
+ "destination": "Destination",
"availableTooltip": "Your supply position in the lending pool",
"confirmTitle": "Confirm Withdrawal",
"confirmDescription": "Withdraw %{amount} %{asset} from the lending pool",
@@ -2680,8 +2706,12 @@
},
"repay": {
"title": "Repay",
+ "subtitle": "Repay outstanding loan balance",
"amount": "Amount",
+ "asset": "Asset",
"outstanding": "Outstanding debt",
+ "repaymentType": "Repayment Type",
+ "full": "Full",
"fullRepayment": "Full repayment",
"partialRepayment": "Partial repayment",
"confirmTitle": "Confirm Repayment",
@@ -2712,12 +2742,16 @@
"softLiquidation": "Soft Liquidation",
"hardLiquidation": "Hard Liquidation",
"safe": "Safe",
+ "risky": "Risky",
+ "liquidation": "Liquidation",
"warning": "Warning",
"danger": "Danger"
},
"egress": {
"title": "Withdraw from Chainflip",
+ "subtitle": "Withdraw from your free balance to your wallet",
"amount": "Amount",
+ "asset": "Asset",
"destination": "Destination Address",
"destinationPlaceholder": "Enter destination address",
"available": "Available Free Balance",
@@ -2766,6 +2800,57 @@
"signing": "Sign transaction",
"confirming": "Confirming on-chain"
}
+ },
+ "dashboard": {
+ "freeBalance": "Free Balance",
+ "supplied": "Supplied",
+ "collateral": "Collateral",
+ "borrowed": "Borrowed",
+ "loanHealth": "Loan Health",
+ "currentLtv": "Current LTV %{ltv}",
+ "liquidationDistance": "Liquidation Distance",
+ "borrowingPower": "Borrowing Power",
+ "available": "Available",
+ "yourNextSteps": "Your Next Steps",
+ "nextStepsSupplyOrCollateral": "Choose whether you want to start earning yield or provide collateral for borrowing.",
+ "nextStepsCollateral": "Provide collateral to start borrowing.",
+ "nextStepsBorrow": "You can now borrow against your collateral.",
+ "noEarningPositions": "No earning positions yet",
+ "noEarningPositionsDescription": "Supply from your free balance to start earning yield from borrower demand.",
+ "provideCollateral": "Provide collateral to start borrowing",
+ "provideCollateralDescription": "Move assets from your free balance into collateral. Your collateral determines how much you can borrow.",
+ "noActiveLoans": "No Active Loans",
+ "noActiveLoansDescription": "Post collateral first, then open borrow against it.",
+ "cantRepay": "Can't Repay?",
+ "startLiquidation": "Start Liquidation",
+ "topupAsset": "Top-up Asset",
+ "getStarted": "Get Started",
+ "depositFirstAsset": "Deposit your first asset to get started",
+ "depositFirstAssetDescription": "Your first deposit handles everything - account creation, FLIP funding for State Chain gas, and a recovery address for the chain you deposit from. Future deposits skip all of this.",
+ "requiresFlip": "Requires 2 FLIP for one-time account funding",
+ "earnYield": "Earn Yield",
+ "earnYieldDescription": "Supply assets into a lending pool and earn variable APY. Yields auto-compound, no action needed after supplying.",
+ "borrowAgainstCollateral": "Borrow Against Collateral",
+ "borrowAgainstCollateralDescription": "Post BTC, ETH, or SOL as collateral and borrow stablecoins up to the target LTV.",
+ "estimatedYearlyEarnings": "Est. Yearly Earnings",
+ "freeBalanceTooltip": "Your available balance on Chainflip State Chain. Deposit from your wallet, or withdraw back.",
+ "suppliedTooltip": "Assets supplied to lending pools earning yield.",
+ "collateralTooltip": "Assets posted as collateral for borrowing.",
+ "borrowedTooltip": "Outstanding loan balances.",
+ "loanHealthTooltip": "Your loan-to-value ratio. Keep it below the target LTV to avoid liquidation.",
+ "borrowingPowerTooltip": "How much more you can borrow based on your collateral.",
+ "deposit": "Deposit",
+ "withdraw": "Withdraw",
+ "supply": "Supply",
+ "addCollateral": "Add Collateral",
+ "borrow": "Borrow",
+ "repay": "Repay",
+ "apy": "APY",
+ "borrowRate": "Borrow Rate",
+ "viewDashboard": "View Dashboard",
+ "lendingMarkets": "Lending Markets",
+ "asset": "Asset",
+ "lendingMarketsDescription": "Each pool is isolated per asset. Pick a market to see what you could earn or borrow before you deposit."
}
},
"chart": {
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx
index 974b7a5f61c..fa2d259d877 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx
@@ -1,10 +1,11 @@
import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons'
-import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
+import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { BorrowMachineCtx } from './BorrowMachineContext'
import { BorrowStepper } from './BorrowStepper'
@@ -30,6 +31,7 @@ type BorrowConfirmProps = {
export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
@@ -89,6 +91,11 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -135,17 +142,29 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -258,24 +277,42 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.borrow.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.borrow.asset')}
+
+
+ {asset.name}
+
+
+
+
+ {translate('chainflipLending.borrow.amount')}
+
-
- {translate('chainflipLending.borrow.projectedLtv')}: {projectedLtvPercent}%
+
+
+
+ {translate('chainflipLending.borrow.projectedLtv')}
+
+
+ {projectedLtvPercent}%
@@ -283,25 +320,32 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx
index 9fe127d95d1..61080ec5d75 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx
@@ -33,6 +33,8 @@ type BorrowInputProps = {
onAssetChange: (assetId: AssetId) => void
}
+const DEFAULT_RISKY_LTV = 0.8
+
export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
const translate = useTranslate()
const {
@@ -100,6 +102,12 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
const projectedLtvDecimal = useMemo(() => projectedLtvBps / 10000, [projectedLtvBps])
+ const riskyLtv = thresholds?.target ?? DEFAULT_RISKY_LTV
+ const projectedLtvColor = useMemo(
+ () => (projectedLtvDecimal > riskyLtv ? 'red.500' : 'text.base'),
+ [projectedLtvDecimal, riskyLtv],
+ )
+
const assetIds = useMemo(() => Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[], [])
const assets = useAppSelector(selectAssets)
@@ -256,7 +264,12 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
fontWeight='medium'
/>
-
+
)}
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx
index 3723cbdd72d..b6e6ef10b37 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx
@@ -1,10 +1,11 @@
import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons'
-import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
+import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { CollateralMachineCtx } from './CollateralMachineContext'
import { CollateralStepper } from './CollateralStepper'
@@ -29,6 +30,7 @@ type CollateralConfirmProps = {
export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -73,6 +75,11 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -129,17 +136,29 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -252,56 +271,74 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
return (
-
-
-
-
-
-
-
-
- {translate(
- isAddMode
- ? 'chainflipLending.collateral.confirmAddTitle'
- : 'chainflipLending.collateral.confirmRemoveTitle',
- )}
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.collateral.asset')}
-
- {translate(
- isAddMode
- ? 'chainflipLending.collateral.confirmAddDescription'
- : 'chainflipLending.collateral.confirmRemoveDescription',
- {
- amount: collateralAmountCryptoPrecision,
- asset: asset.symbol,
- },
- )}
+
+ {asset.name}
+
+
+
+
+ {translate('chainflipLending.collateral.amount')}
-
-
+
+
+ {translate('chainflipLending.collateral.action')}
+
+
+ {translate(
+ isAddMode
+ ? 'chainflipLending.collateral.add'
+ : 'chainflipLending.collateral.remove',
+ )}
+
+
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx
index 518e3e7e9e9..1ab09a02ebc 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx
@@ -1,5 +1,6 @@
-import { Box, Flex, Text, VStack } from '@chakra-ui/react'
+import { Box, Circle, Flex, Icon, Text } from '@chakra-ui/react'
import { memo, useMemo } from 'react'
+import { TbSkull } from 'react-icons/tb'
import { useTranslate } from 'react-polyglot'
import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds'
@@ -9,161 +10,169 @@ type LtvGaugeProps = {
projectedLtv?: number
}
-const GAUGE_HEIGHT = '12px'
-const MARKER_HEIGHT = '24px'
-const ltvToPercent = (ltv: number): string => `${Math.min(Math.max(ltv * 100, 0), 100)}%`
+const GAUGE_HEIGHT = '10px'
+const THUMB_SIZE = '20px'
-const ltvToDisplayPercent = (ltv: number): string =>
- `${(Math.min(Math.max(ltv, 0), 1) * 100).toFixed(1)}%`
+const DEFAULT_HARD_LIQUIDATION_LTV = 0.93
-const DEFAULT_SOFT_LIQUIDATION_LTV = 0.8
-const DEFAULT_HARD_LIQUIDATION_LTV = 0.9
+// Risky threshold sits between soft and hard liquidation - use topup or midpoint
+const DEFAULT_RISKY_LTV = 0.8
-const getStatusColor = (
- ltv: number,
- softLiquidationLtv: number,
- hardLiquidationLtv: number,
-): string => {
+const ltvToPercent = (ltv: number): number => Math.min(Math.max(ltv * 100, 0), 100)
+
+const getStatusColor = (ltv: number, riskyLtv: number, hardLiquidationLtv: number): string => {
if (ltv >= hardLiquidationLtv) return 'red.500'
- if (ltv >= softLiquidationLtv) return 'yellow.500'
+ if (ltv >= riskyLtv) return 'yellow.500'
return 'green.500'
}
-const getStatusKey = (
- ltv: number,
- softLiquidationLtv: number,
- hardLiquidationLtv: number,
-): string => {
- if (ltv >= hardLiquidationLtv) return 'chainflipLending.ltv.danger'
- if (ltv >= softLiquidationLtv) return 'chainflipLending.ltv.warning'
- return 'chainflipLending.ltv.safe'
+type LegendItem = {
+ labelKey: string
+ color: string
}
+const legendItems: LegendItem[] = [
+ { labelKey: 'chainflipLending.ltv.safe', color: 'green.500' },
+ { labelKey: 'chainflipLending.ltv.risky', color: 'yellow.500' },
+ { labelKey: 'chainflipLending.ltv.liquidation', color: 'red.500' },
+]
+
export const LtvGauge = memo(({ currentLtv, projectedLtv }: LtvGaugeProps) => {
const translate = useTranslate()
const { thresholds } = useChainflipLtvThresholds()
- const softLiquidationLtv = thresholds?.softLiquidation ?? DEFAULT_SOFT_LIQUIDATION_LTV
+ // Zone boundaries: safe (0 -> risky), risky (risky -> hard liq), liquidation (hard liq -> 100%)
+ const riskyLtv = thresholds?.target ?? DEFAULT_RISKY_LTV
const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV
- const statusColor = useMemo(
- () => getStatusColor(currentLtv, softLiquidationLtv, hardLiquidationLtv),
- [currentLtv, softLiquidationLtv, hardLiquidationLtv],
- )
- const statusKey = useMemo(
- () => getStatusKey(currentLtv, softLiquidationLtv, hardLiquidationLtv),
- [currentLtv, softLiquidationLtv, hardLiquidationLtv],
+ const thumbPosition = useMemo(() => ltvToPercent(currentLtv), [currentLtv])
+
+ const projectedThumbPosition = useMemo(
+ () => (projectedLtv !== undefined ? ltvToPercent(projectedLtv) : undefined),
+ [projectedLtv],
)
- const gaugeGradient = useMemo(
- () =>
- `linear(to-r, green.500 0%, green.500 ${softLiquidationLtv * 100}%, yellow.500 ${
- softLiquidationLtv * 100
- }%, yellow.500 ${hardLiquidationLtv * 100}%, red.500 ${
- hardLiquidationLtv * 100
- }%, red.800 100%)`,
- [softLiquidationLtv, hardLiquidationLtv],
+ // Segment widths as percentages
+ const safeWidth = useMemo(() => riskyLtv * 100, [riskyLtv])
+ const riskyWidth = useMemo(
+ () => (hardLiquidationLtv - riskyLtv) * 100,
+ [hardLiquidationLtv, riskyLtv],
)
+ const liquidationWidth = useMemo(() => (1 - hardLiquidationLtv) * 100, [hardLiquidationLtv])
- const thresholdMarkers = useMemo(() => {
- if (!thresholds) return []
- return [
- {
- value: thresholds.target,
- labelKey: 'chainflipLending.ltv.target',
- color: 'green.500',
- },
- {
- value: thresholds.softLiquidation,
- labelKey: 'chainflipLending.ltv.softLiquidation',
- color: 'yellow.500',
- },
- {
- value: thresholds.hardLiquidation,
- labelKey: 'chainflipLending.ltv.hardLiquidation',
- color: 'red.500',
- },
- ]
- }, [thresholds])
+ // Position of skull icon at the hard liquidation boundary
+ const skullPosition = useMemo(() => hardLiquidationLtv * 100, [hardLiquidationLtv])
return (
-
-
+ {/* Bar track */}
+
-
+ {/* Safe segment (green) */}
+
+ {/* Risky segment (yellow) */}
+
+ {/* Liquidation segment (red) */}
+
+
+
+ {/* Filled portion up to current LTV */}
+ = 100 ? 'full' : '0'}
+ overflow='hidden'
+ transition='width 0.3s ease'
+ >
+ 0 ? (100 / thumbPosition) * 100 : 100}%`}>
+
+
+
+
+ {/* Skull icon at hard liquidation boundary */}
+ >
+
+
+
+
- {projectedLtv !== undefined && (
+ {/* Projected LTV thumb (dashed circle) */}
+ {projectedThumbPosition !== undefined && (
+ top='50%'
+ left={`${projectedThumbPosition}%`}
+ transform='translate(-50%, -50%)'
+ zIndex={3}
+ transition='left 0.3s ease'
+ >
+
+
)}
- {thresholdMarkers.map(marker => (
-
- ))}
+ {/* Current LTV thumb */}
+
+
+
-
-
-
- {ltvToDisplayPercent(currentLtv)}
-
-
- {translate(statusKey)}
-
-
-
- {thresholdMarkers.map(marker => (
-
-
-
- {translate(marker.labelKey)}
-
-
- ))}
-
-
+ {/* Percentage labels under the bar */}
+
+
+ 0%
+
+
+ 100%
+
+
+
+ {/* Legend */}
+
+ {legendItems.map(item => (
+
+
+
+ {translate(item.labelKey)}
+
+
+ ))}
+
)
})
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx
index 59762893b31..ce18f0dcc78 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx
@@ -1,10 +1,20 @@
import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons'
-import { Badge, Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
+import {
+ Badge,
+ Button,
+ CardBody,
+ CardFooter,
+ Divider,
+ Flex,
+ HStack,
+ VStack,
+} from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { useRepayActionCenter } from './hooks/useRepayActionCenter'
import { useRepayConfirmation } from './hooks/useRepayConfirmation'
@@ -29,6 +39,7 @@ type RepayConfirmProps = {
export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
@@ -73,6 +84,11 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -126,17 +142,29 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -249,50 +277,86 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.repay.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.repay.asset')}
+
+
+ {asset.name}
+
+
+
+
+ {translate('chainflipLending.repay.amount')}
+
- {isFullRepayment && (
- {translate('chainflipLending.repay.fullRepayment')}
- )}
+
+
+
+ {translate('chainflipLending.repay.repaymentType')}
+
+
+
+ {translate(
+ isFullRepayment
+ ? 'chainflipLending.repay.fullRepayment'
+ : 'chainflipLending.repay.partialRepayment',
+ )}
+
+ {isFullRepayment && (
+
+ {translate('chainflipLending.repay.full')}
+
+ )}
+
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx
index 600103d31b8..4f681128d72 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/RepayInput.tsx
@@ -1,4 +1,4 @@
-import { Button, CardBody, CardFooter, Flex, Stack, Switch, VStack } from '@chakra-ui/react'
+import { Box, Button, CardBody, CardFooter, Flex, Stack, Switch, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { BigAmount } from '@shapeshiftoss/utils'
import { useCallback, useMemo, useState } from 'react'
@@ -189,12 +189,19 @@ export const RepayInput = ({ assetId }: RepayInputProps) => {
{translate('chainflipLending.repay.outstanding')}
-
+
+
+
+
@@ -271,18 +278,20 @@ export const RepayInput = ({ assetId }: RepayInputProps) => {
color='text.subtle'
/>
- {!isFullRepayment && (
-
- )}
+
+ {!isFullRepayment && (
+
+ )}
+
diff --git a/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx
index 00e9f2828c5..22411043698 100644
--- a/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Deposit/DepositConfirm.tsx
@@ -6,6 +6,7 @@ import { BigAmount } from '@shapeshiftoss/utils'
import { skipToken, useQuery, useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { DepositMachineCtx } from './DepositMachineContext'
import { DepositStepper } from './DepositStepper'
@@ -47,6 +48,7 @@ type DepositConfirmProps = {
export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { accountNumber, scAccount } = useChainflipLendingAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -167,6 +169,11 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => {
closeModal()
}, [scAccount, queryClient, actorRef, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -231,17 +238,29 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -354,54 +373,92 @@ export const DepositConfirm = memo(({ assetId }: DepositConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.deposit.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.deposit.asset')}
+
+
+ {asset.name}
+
+
+
+
+ {translate('chainflipLending.deposit.amount')}
+
+
+
+
+ {translate('chainflipLending.deposit.freeBalance')}
+
+
{effectiveRefundAddress && (
- <>
-
-
-
- {translate('chainflipLending.deposit.refundAddress.label')}
+
+
+ {translate('chainflipLending.deposit.recoveryAddress')}
+
+
+
+
-
-
-
-
-
-
- >
+
+
)}
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx
index b3cd5205d8f..4a734e4512f 100644
--- a/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Egress/EgressConfirm.tsx
@@ -5,6 +5,7 @@ import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { EgressMachineCtx } from './EgressMachineContext'
import { EgressStepper } from './EgressStepper'
@@ -31,6 +32,7 @@ type EgressConfirmProps = {
export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -80,6 +82,11 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -144,17 +151,29 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -267,60 +286,79 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.egress.confirmTitle')}
-
+
+
+
+
+
+
+
+ {translate('chainflipLending.egress.asset')}
+
+
+ {asset.name}
+
+
+
+
+ {translate('chainflipLending.egress.amount')}
+
+
+
{destinationAddress && (
- <>
-
-
-
- {translate('chainflipLending.egress.receiveAddress')}
+
+
+ {translate('chainflipLending.egress.receiveAddress')}
+
+
+
+
-
-
-
-
-
-
- >
+
+
)}
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx
index 18a18f7d673..5c5f458cf7f 100644
--- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyConfirm.tsx
@@ -1,10 +1,11 @@
import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons'
-import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
+import { Box, Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
-import { memo, useCallback } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { useSupplyActionCenter } from './hooks/useSupplyActionCenter'
import { useSupplyConfirmation } from './hooks/useSupplyConfirmation'
@@ -18,7 +19,9 @@ import { CircularProgress } from '@/components/CircularProgress/CircularProgress
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useModal } from '@/hooks/useModal/useModal'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
import { reactQueries } from '@/react-queries'
import { selectAssetById } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
@@ -29,6 +32,7 @@ type SupplyConfirmProps = {
export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -47,6 +51,13 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
const stepConfirmed = SupplyMachineCtx.useSelector(s => s.context.stepConfirmed)
const isConfirming = SupplyMachineCtx.useSelector(s => s.matches('confirming'))
+ const { pools } = useChainflipLendingPools()
+
+ const supplyApyPercent = useMemo(() => {
+ const pool = pools.find(p => p.assetId === assetId)
+ return pool ? bnOrZero(pool.supplyApy).times(100).toFixed(2) : null
+ }, [pools, assetId])
+
useSupplySign()
useSupplyConfirmation()
useSupplyActionCenter()
@@ -72,6 +83,11 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
closeModal()
}, [scAccount, queryClient, actorRef, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -117,17 +133,29 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -240,47 +268,101 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.supply.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.supply.asset')}
+
+
+
+
+ {asset.symbol}
+
+
+
+
+
+ {translate('chainflipLending.supply.amount')}
+
+
+
+ {supplyApyPercent !== null && (
+
+
+ {translate('chainflipLending.supply.poolApy')}
+
+
+ {supplyApyPercent}%
+
+
+ )}
+
+
+ {translate('chainflipLending.supply.destination')}
+
+
+ {translate('chainflipLending.supply.lendingPool', { asset: asset.symbol })}
+
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx
index 2d5ca4de85d..f0f54c52f25 100644
--- a/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Supply/SupplyInput.tsx
@@ -1,5 +1,6 @@
-import { Button, CardBody, CardFooter, Flex, Stack, VStack } from '@chakra-ui/react'
+import { Box, Button, CardBody, CardFooter, Divider, Flex, Stack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
+import { usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import { BigAmount } from '@shapeshiftoss/utils'
import { useCallback, useMemo, useState } from 'react'
@@ -10,8 +11,7 @@ import { useTranslate } from 'react-polyglot'
import { SupplyMachineCtx } from './SupplyMachineContext'
import { Amount } from '@/components/Amount/Amount'
-import { TradeAssetSelect } from '@/components/AssetSelection/AssetSelection'
-import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { AssetIcon } from '@/components/AssetIcon'
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
@@ -19,12 +19,16 @@ import { useModal } from '@/hooks/useModal/useModal'
import { useToggle } from '@/hooks/useToggle/useToggle'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
import { useChainflipMinimumSupply } from '@/pages/ChainflipLending/hooks/useChainflipMinimumSupply'
+import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
import { selectAssetById, selectAssets } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
+const STABLECOIN_ASSET_IDS: AssetId[] = [usdcAssetId, usdtAssetId]
+
type SupplyInputProps = {
assetId: AssetId
onAssetChange: (assetId: AssetId) => void
@@ -79,6 +83,46 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
const cryptoValue = isFiat ? cryptoFromFiat : inputValue
const fiatValue = isFiat ? inputValue : fiatFromCrypto
+ const { pools } = useChainflipLendingPools()
+ const { supplyPositions } = useChainflipSupplyPositions()
+
+ const poolForAsset = useMemo(() => pools.find(p => p.assetId === assetId), [pools, assetId])
+
+ const currentPositionCrypto = useMemo(() => {
+ const position = supplyPositions.find(p => p.assetId === assetId)
+ return position?.totalAmountCryptoPrecision ?? '0'
+ }, [supplyPositions, assetId])
+
+ const currentPositionFiat = useMemo(() => {
+ const position = supplyPositions.find(p => p.assetId === assetId)
+ return position?.totalAmountFiat ?? '0'
+ }, [supplyPositions, assetId])
+
+ const supplyApyPercent = useMemo(
+ () => (poolForAsset ? bnOrZero(poolForAsset.supplyApy).times(100).toFixed(2) : null),
+ [poolForAsset],
+ )
+
+ const poolSharePercent = useMemo(() => {
+ if (!poolForAsset) return '0.00'
+ const totalPoolCrypto = bnOrZero(poolForAsset.totalAmountCryptoPrecision)
+ if (totalPoolCrypto.isZero()) return '0.00'
+ const userCrypto = bnOrZero(currentPositionCrypto).plus(bnOrZero(cryptoValue))
+ return userCrypto
+ .div(totalPoolCrypto.plus(bnOrZero(cryptoValue)))
+ .times(100)
+ .toFixed(2)
+ }, [poolForAsset, currentPositionCrypto, cryptoValue])
+
+ const isStablecoin = useMemo(() => STABLECOIN_ASSET_IDS.includes(assetId), [assetId])
+
+ const estYearlyEarningsFiat = useMemo(() => {
+ if (!poolForAsset || !marketData?.price) return null
+ const apyDecimal = bnOrZero(poolForAsset.supplyApy)
+ if (apyDecimal.isZero()) return null
+ return bnOrZero(cryptoValue).times(marketData.price).times(apyDecimal).toFixed(2)
+ }, [poolForAsset, cryptoValue, marketData?.price])
+
const { minSupply, isLoading: isMinSupplyLoading } = useChainflipMinimumSupply(assetId)
const isBelowMinimum = useMemo(() => {
@@ -119,11 +163,6 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
})
}, [buyAssetSearch, onAssetChange, lendingAssets])
- const handleAssetChange = useCallback(
- (asset: Asset) => onAssetChange(asset.assetId),
- [onAssetChange],
- )
-
const handleInputChange = useCallback((values: NumberFormatValues) => {
setInputValue(values.value)
}, [])
@@ -188,7 +227,7 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
return translate('chainflipLending.supply.minimumSupply', {
amount: `${bnOrZero(minSupply).decimalPlaces(2).toFixed()} ${asset?.symbol}`,
})
- return translate('chainflipLending.supply.title')
+ return translate('common.next')
}, [exceedsBalance, isBelowMinimum, minSupply, asset?.symbol, translate])
if (!asset) return null
@@ -197,16 +236,79 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
-
+ {/* Asset to supply label */}
+
+ {translate('chainflipLending.supply.assetToSupply')}
+
+
+ {/* Asset card */}
+
+
+
+
+
+
+ {asset.name}
+
+
+
+
+ {availableFiat !== undefined && (
+
+ )}
+
+
+
+ {/* Stats row - Pool APY and Current Position in bordered boxes */}
+
+ {supplyApyPercent !== null && (
+
+
+
+ {translate('chainflipLending.supply.poolApy')}
+
+
+ {supplyApyPercent}%
+
+
+
+ )}
+
+
+
+ {translate('chainflipLending.supply.currentPosition')}
+
+ {bnOrZero(currentPositionCrypto).gt(0) ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Amount label */}
{translate('chainflipLending.supply.amount')}
@@ -229,7 +331,7 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
onValueChange={handleInputChange}
style={{
flex: 1,
- fontSize: '1.25rem',
+ fontSize: '1.5rem',
fontWeight: 'bold',
background: 'transparent',
border: 'none',
@@ -238,60 +340,109 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
}}
/>
{!isFiat && (
-
- {asset.symbol}
-
+
+
+
+ {asset.symbol}
+
+
)}
- {marketData?.price && (
-
- )}
-
-
-
-
- {translate('chainflipLending.supply.available')}
-
-
-
-
- {availableFiat !== undefined && (
-
- )}
+ {/* Fiat estimate + Balance/Max row */}
+
+ {marketData?.price ? (
+
+ ) : (
+
+ )}
+
+
+ {translate('common.balance')}:
+
-
-
+
+
-
+
+
+
+
+ {/* Info rows */}
+
+ {estYearlyEarningsFiat !== null && !bnOrZero(cryptoValue).isZero() && (
+
+
+ {translate('chainflipLending.dashboard.estimatedYearlyEarnings')}
+
+
+
+ )}
+
+
+
+ {translate('chainflipLending.supply.poolShare')}
+
+
+ {poolSharePercent}%
+
+
+
+
+
+ {translate('chainflipLending.supply.autoCompounding')}
+
+
+ {translate('chainflipLending.supply.enabled')}
+
+
+
+
+
+ {translate('chainflipLending.supply.riskBand')}
+
+
+ {isStablecoin
+ ? translate('chainflipLending.supply.conservativeStablecoin')
+ : translate('chainflipLending.supply.volatileAsset')}
+
+
+
{!hasFreeBalance && (
diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx
index 1034d8f6fa6..9c8c740eb53 100644
--- a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx
@@ -1,10 +1,11 @@
import { ArrowForwardIcon, CheckCircleIcon } from '@chakra-ui/icons'
-import { Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
+import { Button, CardBody, CardFooter, Divider, Flex, HStack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { flipAssetId } from '@shapeshiftoss/caip'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { useWithdrawActionCenter } from './hooks/useWithdrawActionCenter'
import { useWithdrawConfirmation } from './hooks/useWithdrawConfirmation'
@@ -29,6 +30,7 @@ type WithdrawConfirmProps = {
export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -71,6 +73,11 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
@@ -116,17 +123,29 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
px={6}
py={4}
>
-
+
+
+
+
)
@@ -239,47 +258,67 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.withdraw.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.withdraw.amount')}
+
+
+
+ {translate('chainflipLending.withdraw.destination')}
+
+
+ {translate('chainflipLending.freeBalance')}
+
+
+
-
)
diff --git a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx
index d1d16a66570..37bbe96a308 100644
--- a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx
+++ b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx
@@ -1,6 +1,6 @@
import { Button, Card, CardBody, Container, Flex, Heading, Skeleton, Stack } from '@chakra-ui/react'
import { ethAssetId } from '@shapeshiftoss/caip'
-import { useCallback } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { useNavigate } from 'react-router-dom'
@@ -12,13 +12,59 @@ import { PageBackButton, PageHeader } from '@/components/Layout/Header/PageHeade
import { Text } from '@/components/Text'
import { WalletActions } from '@/context/WalletProvider/actions'
import { useWallet } from '@/hooks/useWallet/useWallet'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
+import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
const responsiveFlex = { base: 'auto', lg: 1 }
const containerPaddingTop = { base: 0, md: 8 }
-export const ChainflipLendingHeader = () => {
+type SummaryCardProps = {
+ value: string
+ labelKey: string
+ tooltipKey: string
+ isLoading: boolean
+ labelColor?: string
+ 'data-testid'?: string
+}
+
+const SummaryCard = ({
+ value,
+ labelKey,
+ tooltipKey,
+ isLoading,
+ labelColor,
+ 'data-testid': testId,
+}: SummaryCardProps) => {
+ const translate = useTranslate()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+type ChainflipLendingHeaderProps = {
+ tabIndex?: number
+}
+
+export const ChainflipLendingHeader = memo(({ tabIndex }: ChainflipLendingHeaderProps) => {
const translate = useTranslate()
const navigate = useNavigate()
const { dispatch: walletDispatch } = useWallet()
@@ -33,8 +79,30 @@ export const ChainflipLendingHeader = () => {
[walletDispatch],
)
- const { totalSuppliedFiat, availableLiquidityFiat, totalBorrowedFiat, isLoading } =
- useChainflipLendingPools()
+ const {
+ totalSuppliedFiat,
+ availableLiquidityFiat,
+ totalBorrowedFiat,
+ isLoading: isPoolsLoading,
+ } = useChainflipLendingPools()
+
+ const { totalFiat: freeBalanceTotalFiat, isLoading: isFreeBalancesLoading } =
+ useChainflipFreeBalances()
+
+ const { supplyPositions, isLoading: isPositionsLoading } = useChainflipSupplyPositions()
+
+ const suppliedTotalFiat = useMemo(
+ () => supplyPositions.reduce((sum, p) => sum.plus(p.totalAmountFiat), bnOrZero(0)).toFixed(2),
+ [supplyPositions],
+ )
+
+ const {
+ totalCollateralFiat,
+ totalBorrowedFiat: userBorrowedFiat,
+ isLoading: isLoanLoading,
+ } = useChainflipLoanAccount()
+
+ const isUserDataLoading = isFreeBalancesLoading || isPositionsLoading || isLoanLoading
return (
<>
@@ -49,7 +117,7 @@ export const ChainflipLendingHeader = () => {
-
+
@@ -65,58 +133,82 @@ export const ChainflipLendingHeader = () => {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {accountId && tabIndex === 0 ? (
+ <>
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ {!accountId && (
+
+
+
+ )}
+ >
+ )}
- {!accountId && (
-
-
-
- )}
>
)
-}
+})
diff --git a/src/pages/ChainflipLending/components/Dashboard.tsx b/src/pages/ChainflipLending/components/Dashboard.tsx
new file mode 100644
index 00000000000..4a30ca5daf5
--- /dev/null
+++ b/src/pages/ChainflipLending/components/Dashboard.tsx
@@ -0,0 +1,66 @@
+import { Flex, Stack } from '@chakra-ui/react'
+import { memo, useMemo } from 'react'
+
+import { bnOrZero } from '@/lib/bignumber/bignumber'
+import {
+ BorrowedSection,
+ CollateralSection,
+ FreeBalanceSection,
+ SuppliedSection,
+} from '@/pages/ChainflipLending/components/DashboardSections'
+import { DashboardSidebar } from '@/pages/ChainflipLending/components/DashboardSidebar'
+import { InitView } from '@/pages/ChainflipLending/components/InitView'
+import { LoanHealth } from '@/pages/ChainflipLending/components/LoanHealth'
+import { useChainflipAccount } from '@/pages/ChainflipLending/hooks/useChainflipAccount'
+import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
+
+export const Dashboard = memo(() => {
+ const { isFunded, isLpRegistered } = useChainflipAccount()
+ const { freeBalances } = useChainflipFreeBalances()
+ const { supplyPositions } = useChainflipSupplyPositions()
+ const { collateralWithFiat, loansWithFiat } = useChainflipLoanAccount()
+
+ const hasFreeBalance = useMemo(
+ () => freeBalances.some(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)),
+ [freeBalances],
+ )
+
+ const hasAnyPosition = useMemo(
+ () =>
+ hasFreeBalance ||
+ supplyPositions.length > 0 ||
+ collateralWithFiat.length > 0 ||
+ loansWithFiat.length > 0,
+ [hasFreeBalance, supplyPositions, collateralWithFiat, loansWithFiat],
+ )
+
+ // Show init view for users with no positions (wallet is connected since Markets.tsx handles no-wallet)
+ const showInitView = useMemo(() => {
+ if (!isFunded && !isLpRegistered) return true
+ return !hasAnyPosition
+ }, [isFunded, isLpRegistered, hasAnyPosition])
+
+ if (showInitView) {
+ return
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+})
diff --git a/src/pages/ChainflipLending/components/DashboardSections.tsx b/src/pages/ChainflipLending/components/DashboardSections.tsx
new file mode 100644
index 00000000000..8cfe95e3cbc
--- /dev/null
+++ b/src/pages/ChainflipLending/components/DashboardSections.tsx
@@ -0,0 +1,730 @@
+import { Badge, Button, Card, CardBody, Flex, HStack, Skeleton, Stack } from '@chakra-ui/react'
+import type { AssetId } from '@shapeshiftoss/caip'
+import { memo, useCallback, useMemo } from 'react'
+import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
+
+import { Amount } from '@/components/Amount/Amount'
+import { AssetIcon } from '@/components/AssetIcon'
+import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { RawText, Text } from '@/components/Text'
+import { useModal } from '@/hooks/useModal/useModal'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
+import {
+ CHAINFLIP_LENDING_ASSET_BY_ASSET_ID,
+ CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET,
+} from '@/lib/chainflip/constants'
+import type { ChainflipFreeBalanceWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
+import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
+import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import type {
+ CollateralWithFiat,
+ LoanWithFiat,
+} from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import type { ChainflipSupplyPositionWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
+import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
+import { selectAssetById } from '@/state/slices/assetsSlice/selectors'
+import { useAppSelector } from '@/state/store'
+
+const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[]
+
+// Shared grid for consistent alignment across dashboard sections
+const dashboardRowGrid = '1fr 100px 120px'
+
+type AssetRowProps = {
+ assetId: AssetId
+ children: React.ReactNode
+ badge?: React.ReactNode
+ onClick?: () => void
+}
+
+const AssetRow = ({ assetId, children, badge, onClick }: AssetRowProps) => {
+ const asset = useAppSelector(state => selectAssetById(state, assetId))
+ if (!asset) return null
+
+ return (
+
+ )
+}
+
+type SectionHeaderProps = {
+ titleKey: string
+ tooltipKey: string
+ totalFiat: string
+ isLoading: boolean
+ primaryAction?: { labelKey: string; handleClick: () => void; testId?: string }
+ secondaryAction?: { labelKey: string; handleClick: () => void; prefix?: string }
+}
+
+const SectionHeader = ({
+ titleKey,
+ tooltipKey,
+ totalFiat,
+ isLoading,
+ primaryAction,
+ secondaryAction,
+}: SectionHeaderProps) => {
+ const translate = useTranslate()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {primaryAction && (
+
+ )}
+ {secondaryAction && (
+
+ )}
+
+
+ )
+}
+
+type EmptyStateProps = {
+ titleKey: string
+ descriptionKey: string
+ actionLabelKey: string
+ onAction: () => void
+ actionTestId?: string
+}
+
+const EmptyState = ({
+ titleKey,
+ descriptionKey,
+ actionLabelKey,
+ onAction,
+ actionTestId,
+}: EmptyStateProps) => {
+ const translate = useTranslate()
+
+ return (
+
+
+
+
+
+ )
+}
+
+// Free Balance Section
+const FreeBalanceRow = ({ balance }: { balance: ChainflipFreeBalanceWithFiat }) => {
+ const assetId = balance.assetId
+ const asset = useAppSelector(state => (assetId ? selectAssetById(state, assetId) : undefined))
+ const navigate = useNavigate()
+
+ const handleRowClick = useCallback(() => {
+ if (!assetId) return
+ navigate(`/chainflip-lending/pool/${assetId}`)
+ }, [navigate, assetId])
+
+ if (!assetId || bnOrZero(balance.balanceCryptoPrecision).isZero()) return null
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export const FreeBalanceSection = memo(() => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+ const { freeBalances, isLoading, totalFiat } = useChainflipFreeBalances()
+
+ const nonZeroBalances = useMemo(
+ () => freeBalances.filter(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)),
+ [freeBalances],
+ )
+
+ const handleDeposit = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'deposit', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleWithdraw = useCallback(() => {
+ const firstWithBalance = nonZeroBalances[0]?.assetId ?? LENDING_ASSET_IDS[0]
+ if (firstWithBalance) {
+ chainflipLendingModal.open({ mode: 'withdrawFromChainflip', assetId: firstWithBalance })
+ }
+ }, [chainflipLendingModal, nonZeroBalances])
+
+ return (
+
+
+
+ 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.withdraw',
+ handleClick: handleWithdraw,
+ }
+ : undefined
+ }
+ />
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : nonZeroBalances.length > 0 ? (
+
+
+ {translate('chainflipLending.dashboard.asset')}
+
+ {translate('common.balance')}
+
+ }>
+ {nonZeroBalances.map(balance =>
+ balance.assetId ? (
+
+ ) : null,
+ )}
+
+
+ ) : null}
+
+
+
+ )
+})
+
+// Supplied Section
+const SuppliedRow = ({
+ position,
+ apy,
+}: {
+ position: ChainflipSupplyPositionWithFiat
+ apy: string
+}) => {
+ const asset = useAppSelector(state => selectAssetById(state, position.assetId))
+ const navigate = useNavigate()
+
+ const handleRowClick = useCallback(() => {
+ navigate(`/chainflip-lending/pool/${position.assetId}`)
+ }, [navigate, position.assetId])
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export const SuppliedSection = memo(() => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+ const { supplyPositions, isLoading } = useChainflipSupplyPositions()
+ const { pools } = useChainflipLendingPools()
+
+ const totalSuppliedFiat = useMemo(
+ () => supplyPositions.reduce((sum, p) => sum.plus(p.totalAmountFiat), bnOrZero(0)).toFixed(2),
+ [supplyPositions],
+ )
+
+ const poolsByAssetId = useMemo(
+ () =>
+ pools.reduce>>((acc, pool) => {
+ if (pool.assetId) acc[pool.assetId] = pool
+ return acc
+ }, {}),
+ [pools],
+ )
+
+ const handleSupply = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'supply', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleWithdraw = useCallback(() => {
+ const firstPosition = supplyPositions[0]
+ if (firstPosition) {
+ chainflipLendingModal.open({ mode: 'withdrawSupply', assetId: firstPosition.assetId })
+ }
+ }, [chainflipLendingModal, supplyPositions])
+
+ return (
+
+
+
+ 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.deposit',
+ handleClick: handleSupply,
+ testId: 'chainflip-lending-supply-action',
+ }
+ : undefined
+ }
+ secondaryAction={
+ supplyPositions.length > 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.withdraw',
+ handleClick: handleWithdraw,
+ }
+ : undefined
+ }
+ />
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : supplyPositions.length > 0 ? (
+
+
+ {translate('chainflipLending.dashboard.asset')}
+
+
+ {translate('chainflipLending.dashboard.supplied')}
+
+
+ }>
+ {supplyPositions.map(position => (
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+})
+
+// Collateral Section
+const CollateralRow = ({
+ collateral,
+ isTopupAsset,
+}: {
+ collateral: CollateralWithFiat
+ isTopupAsset: boolean
+}) => {
+ const translate = useTranslate()
+ const asset = useAppSelector(state => selectAssetById(state, collateral.assetId))
+ const navigate = useNavigate()
+
+ const handleRowClick = useCallback(() => {
+ navigate(`/chainflip-lending/pool/${collateral.assetId}`)
+ }, [navigate, collateral.assetId])
+
+ return (
+
+ {translate('chainflipLending.dashboard.topupAsset')}
+
+ )
+ }
+ onClick={handleRowClick}
+ >
+
+
+
+
+
+
+ )
+}
+
+export const CollateralSection = memo(() => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+ const { collateralWithFiat, totalCollateralFiat, loanAccount, isLoading } =
+ useChainflipLoanAccount()
+
+ const topupAssetId = useMemo(() => {
+ if (!loanAccount?.collateral_topup_asset) return undefined
+ return CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET[loanAccount.collateral_topup_asset.asset]
+ }, [loanAccount?.collateral_topup_asset])
+
+ const handleAddCollateral = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleRemoveCollateral = useCallback(() => {
+ const firstCollateral = collateralWithFiat[0]
+ if (firstCollateral) {
+ chainflipLendingModal.open({ mode: 'removeCollateral', assetId: firstCollateral.assetId })
+ }
+ }, [chainflipLendingModal, collateralWithFiat])
+
+ return (
+
+
+
+ 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.deposit',
+ handleClick: handleAddCollateral,
+ testId: 'chainflip-lending-collateral-action',
+ }
+ : undefined
+ }
+ secondaryAction={
+ collateralWithFiat.length > 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.withdraw',
+ handleClick: handleRemoveCollateral,
+ }
+ : undefined
+ }
+ />
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : collateralWithFiat.length > 0 ? (
+
+
+ {translate('chainflipLending.dashboard.asset')}
+
+ {translate('common.amount')}
+
+ }>
+ {collateralWithFiat.map(collateral => (
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+})
+
+// Borrowed Section
+const BorrowedRow = ({ loan, borrowRate }: { loan: LoanWithFiat; borrowRate: string }) => {
+ const asset = useAppSelector(state => selectAssetById(state, loan.assetId))
+ const navigate = useNavigate()
+
+ const handleRowClick = useCallback(() => {
+ navigate(`/chainflip-lending/pool/${loan.assetId}`)
+ }, [navigate, loan.assetId])
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export const BorrowedSection = memo(() => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+ const { loansWithFiat, totalBorrowedFiat, isLoading } = useChainflipLoanAccount()
+ const { pools } = useChainflipLendingPools()
+
+ const poolsByAssetId = useMemo(
+ () =>
+ pools.reduce>>((acc, pool) => {
+ if (pool.assetId) acc[pool.assetId] = pool
+ return acc
+ }, {}),
+ [pools],
+ )
+
+ const handleBorrow = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleRepay = useCallback(() => {
+ const firstLoan = loansWithFiat[0]
+ if (firstLoan) {
+ chainflipLendingModal.open({
+ mode: 'repay',
+ assetId: firstLoan.assetId,
+ loanId: firstLoan.loanId,
+ })
+ }
+ }, [chainflipLendingModal, loansWithFiat])
+
+ const handleAddCollateral = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleVoluntaryLiquidation = useCallback(() => {
+ const firstLoan = loansWithFiat[0]
+ if (firstLoan) {
+ chainflipLendingModal.open({
+ mode: 'voluntaryLiquidation',
+ assetId: firstLoan.assetId,
+ liquidationAction: 'initiate',
+ })
+ }
+ }, [chainflipLendingModal, loansWithFiat])
+
+ return (
+
+
+
+ 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.borrow',
+ handleClick: handleBorrow,
+ testId: 'chainflip-lending-borrow-action',
+ }
+ : undefined
+ }
+ secondaryAction={
+ loansWithFiat.length > 0
+ ? {
+ labelKey: 'chainflipLending.dashboard.repay',
+ handleClick: handleRepay,
+ prefix: '⟲',
+ }
+ : undefined
+ }
+ />
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : loansWithFiat.length > 0 ? (
+ <>
+
+
+ {translate('chainflipLending.dashboard.asset')}
+
+ {translate('common.amount')}
+
+ }
+ >
+ {loansWithFiat.map(loan => (
+
+ ))}
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ )
+})
diff --git a/src/pages/ChainflipLending/components/DashboardSidebar.tsx b/src/pages/ChainflipLending/components/DashboardSidebar.tsx
new file mode 100644
index 00000000000..9d8e7f64e24
--- /dev/null
+++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx
@@ -0,0 +1,322 @@
+import {
+ Box,
+ Button,
+ Card,
+ CardBody,
+ Center,
+ CircularProgress,
+ CircularProgressLabel,
+ Flex,
+ Stack,
+} from '@chakra-ui/react'
+import type { AssetId } from '@shapeshiftoss/caip'
+import { memo, useCallback, useMemo } from 'react'
+import { useTranslate } from 'react-polyglot'
+
+import borrowGlow from '@/assets/chainflip-lending/borrow-glow.svg'
+import borrowRing1 from '@/assets/chainflip-lending/borrow-ring-1.svg'
+import borrowRingInner from '@/assets/chainflip-lending/borrow-ring-inner.svg'
+import earnGlow from '@/assets/chainflip-lending/earn-glow.svg'
+import earnRingInner from '@/assets/chainflip-lending/earn-ring-inner.svg'
+import earnRingMiddle from '@/assets/chainflip-lending/earn-ring-middle.svg'
+import earnRingOuter from '@/assets/chainflip-lending/earn-ring-outer.svg'
+import refreshIcon from '@/assets/chainflip-lending/refresh-icon.svg'
+import sparklesIcon from '@/assets/chainflip-lending/sparkles-icon.svg'
+import { Amount } from '@/components/Amount/Amount'
+import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { Text } from '@/components/Text'
+import { useModal } from '@/hooks/useModal/useModal'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
+import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
+import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds'
+import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
+
+const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[]
+
+const sidebarPosition = { base: 'relative' as const, lg: 'sticky' as const }
+const sidebarTop = { base: 0, lg: 4 }
+
+export const BorrowingPowerCard = memo(() => {
+ const translate = useTranslate()
+ const { totalCollateralFiat, totalBorrowedFiat } = useChainflipLoanAccount()
+ const { thresholds } = useChainflipLtvThresholds()
+
+ const hasCollateral = useMemo(() => bnOrZero(totalCollateralFiat).gt(0), [totalCollateralFiat])
+
+ const targetLtv = thresholds?.target ?? 0.5
+
+ const maxBorrow = useMemo(
+ () => bnOrZero(totalCollateralFiat).times(targetLtv).toFixed(2),
+ [totalCollateralFiat, targetLtv],
+ )
+
+ const available = useMemo(
+ () => bnOrZero(maxBorrow).minus(totalBorrowedFiat).toFixed(2),
+ [maxBorrow, totalBorrowedFiat],
+ )
+
+ const percentUsed = useMemo(() => {
+ if (bnOrZero(maxBorrow).isZero()) return 0
+ return bnOrZero(totalBorrowedFiat).div(maxBorrow).times(100).toNumber()
+ }, [totalBorrowedFiat, maxBorrow])
+
+ const gaugeColor = useMemo(() => (percentUsed > 80 ? 'red.500' : 'blue.500'), [percentUsed])
+
+ if (!hasCollateral) return null
+
+ return (
+
+
+
+
+
+
+
+
+ {Math.round(percentUsed)}%
+
+
+
+
+
+
+
+
+
+ )
+})
+
+const NextStepsArt = memo(({ colorScheme }: { colorScheme: 'green' | 'purple' }) => {
+ const isGreen = colorScheme === 'green'
+
+ return (
+
+ {isGreen ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ )
+})
+
+export const NextStepsCard = memo(() => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+ const { freeBalances } = useChainflipFreeBalances()
+ const { supplyPositions } = useChainflipSupplyPositions()
+ const { collateralWithFiat, loansWithFiat } = useChainflipLoanAccount()
+
+ const hasFreeBalance = useMemo(
+ () => freeBalances.some(b => b.assetId && bnOrZero(b.balanceCryptoPrecision).gt(0)),
+ [freeBalances],
+ )
+
+ const hasSupply = supplyPositions.length > 0
+ const hasCollateral = collateralWithFiat.length > 0
+ const hasLoans = loansWithFiat.length > 0
+
+ const handleSupply = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'supply', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleAddCollateral = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'addCollateral', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleBorrow = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ // Hide when user has completed all steps
+ if (hasSupply && hasCollateral && hasLoans) return null
+
+ const getContent = () => {
+ if (hasFreeBalance && !hasSupply && !hasCollateral) {
+ return {
+ colorScheme: 'green' as const,
+ descriptionKey: 'chainflipLending.dashboard.nextStepsSupplyOrCollateral',
+ actions: (
+
+
+
+
+ ),
+ }
+ }
+
+ if (hasSupply && !hasCollateral) {
+ return {
+ colorScheme: 'green' as const,
+ descriptionKey: 'chainflipLending.dashboard.nextStepsCollateral',
+ actions: (
+
+ ),
+ }
+ }
+
+ if (hasCollateral && !hasLoans) {
+ return {
+ colorScheme: 'purple' as const,
+ descriptionKey: 'chainflipLending.dashboard.nextStepsBorrow',
+ actions: (
+
+ ),
+ }
+ }
+
+ return null
+ }
+
+ const content = getContent()
+ if (!content) return null
+
+ return (
+
+
+
+
+
+
+ {content.actions}
+
+
+
+ )
+})
+
+export const DashboardSidebar = memo(() => {
+ return (
+
+
+
+
+
+
+ )
+})
diff --git a/src/pages/ChainflipLending/components/InitView.tsx b/src/pages/ChainflipLending/components/InitView.tsx
new file mode 100644
index 00000000000..dc255b827da
--- /dev/null
+++ b/src/pages/ChainflipLending/components/InitView.tsx
@@ -0,0 +1,573 @@
+import {
+ Badge,
+ Box,
+ Button,
+ Card,
+ CardBody,
+ Center,
+ CircularProgress,
+ Flex,
+ Heading,
+ HStack,
+ SimpleGrid,
+ Skeleton,
+ Stack,
+} from '@chakra-ui/react'
+import type { AssetId } from '@shapeshiftoss/caip'
+import { btcAssetId, ethAssetId, solAssetId, usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip'
+import { memo, useCallback, useMemo } from 'react'
+import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
+
+import borrowGlow from '@/assets/chainflip-lending/borrow-glow.svg'
+import borrowRing1 from '@/assets/chainflip-lending/borrow-ring-1.svg'
+import borrowRing2 from '@/assets/chainflip-lending/borrow-ring-2.svg'
+import borrowRing3 from '@/assets/chainflip-lending/borrow-ring-3.svg'
+import borrowRingInner from '@/assets/chainflip-lending/borrow-ring-inner.svg'
+import earnGlow from '@/assets/chainflip-lending/earn-glow.svg'
+import earnRingInner from '@/assets/chainflip-lending/earn-ring-inner.svg'
+import earnRingMiddle from '@/assets/chainflip-lending/earn-ring-middle.svg'
+import earnRingOuter from '@/assets/chainflip-lending/earn-ring-outer.svg'
+import orbitalBtc from '@/assets/chainflip-lending/orbital-btc.svg'
+import orbitalEth from '@/assets/chainflip-lending/orbital-eth.svg'
+import orbitalSol from '@/assets/chainflip-lending/orbital-sol.svg'
+import orbitalTether from '@/assets/chainflip-lending/orbital-tether.svg'
+import orbitalUsdc from '@/assets/chainflip-lending/orbital-usdc.svg'
+import refreshIcon from '@/assets/chainflip-lending/refresh-icon.svg'
+import sparklesIcon from '@/assets/chainflip-lending/sparkles-icon.svg'
+import { Amount } from '@/components/Amount/Amount'
+import { AssetIcon } from '@/components/AssetIcon'
+import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { AssetCell } from '@/components/StakingVaults/Cells'
+import { Text } from '@/components/Text'
+import { useModal } from '@/hooks/useModal/useModal'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
+import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
+import { permillToDecimal } from '@/lib/chainflip/utils'
+import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { selectAssetById } from '@/state/slices/assetsSlice/selectors'
+import { useAppSelector } from '@/state/store'
+
+const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[]
+
+const marketRowGrid = {
+ base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))',
+ md: '200px repeat(5, 1fr)',
+}
+
+const mobileDisplay = { base: 'none', md: 'flex' }
+
+type MarketRowProps = {
+ pool: ChainflipLendingPoolWithFiat
+ onViewMarket: (assetId: AssetId) => void
+}
+
+const MarketRow = memo(({ pool, onViewMarket }: MarketRowProps) => {
+ const asset = useAppSelector(state =>
+ pool.assetId ? selectAssetById(state, pool.assetId) : undefined,
+ )
+ const handleClick = useCallback(() => {
+ if (pool.assetId) onViewMarket(pool.assetId)
+ }, [pool.assetId, onViewMarket])
+
+ const utilisationPercent = useMemo(
+ () => permillToDecimal(pool.pool.utilisation_rate),
+ [pool.pool.utilisation_rate],
+ )
+
+ const utilisationNumber = useMemo(
+ () => bnOrZero(utilisationPercent).times(100).toNumber(),
+ [utilisationPercent],
+ )
+
+ const utilisationColor = useMemo(() => {
+ if (utilisationNumber >= 90) return 'red.400'
+ if (utilisationNumber >= 70) return 'orange.400'
+ return 'blue.400'
+ }, [utilisationNumber])
+
+ if (!pool.assetId) return null
+
+ return (
+
+ )
+})
+
+const AssetConstellation = memo(() => {
+ // Exact positions from Figma design context (2918:4222)
+ // Container is positioned absolutely from left=590px in the original ~1200px wide card
+ // We scale proportionally to fit our responsive layout
+ return (
+
+ {/* Orbital ring SVGs from Figma - ETH blue arc */}
+
+ {/* BTC orange arc */}
+
+ {/* USDC arc */}
+
+ {/* Tether arc */}
+
+ {/* SOL arc */}
+
+ {/* Asset icons - positioned per Figma */}
+ {/* USDC - top left */}
+
+
+
+ {/* ETH - center, largest */}
+
+
+
+ {/* USDT - top right */}
+
+
+
+ {/* BTC - bottom center-left */}
+
+
+
+ {/* SOL - bottom right */}
+
+
+
+
+ )
+})
+
+const MarketsTable = memo(() => {
+ const translate = useTranslate()
+ const navigate = useNavigate()
+ const { pools, isLoading } = useChainflipLendingPools()
+
+ const handleViewMarket = useCallback(
+ (assetId: AssetId) => {
+ navigate(`/chainflip-lending/pool/${assetId}`)
+ },
+ [navigate],
+ )
+
+ const sortedPools = useMemo(
+ () => [...pools].sort((a, b) => bnOrZero(b.supplyApy).minus(a.supplyApy).toNumber()),
+ [pools],
+ )
+
+ const marketRows = useMemo(() => {
+ if (isLoading) {
+ return Array.from({ length: 5 }).map((_, i) => )
+ }
+
+ return sortedPools.map(pool =>
+ pool.assetId ? (
+
+ ) : null,
+ )
+ }, [isLoading, sortedPools, handleViewMarket])
+
+ return (
+
+
+ {translate('chainflipLending.dashboard.lendingMarkets')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {marketRows}
+
+
+ )
+})
+
+type InfoCardProps = {
+ titleKey: string
+ descriptionKey: string
+ accentColor: string
+ 'data-testid'?: string
+}
+
+const InfoCard = memo(
+ ({ titleKey, descriptionKey, accentColor, 'data-testid': testId }: InfoCardProps) => {
+ const isGreen = accentColor === 'green'
+
+ return (
+
+
+
+
+
+
+
+ {/* Art from Figma SVGs */}
+
+ {isGreen ? (
+ <>
+ {/* Earn Yield art - concentric green arcs */}
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+ {/* Borrow art - purple rings with radiating lines */}
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ )
+ },
+)
+
+export const InitView = memo(({ 'data-testid': testId }: { 'data-testid'?: string }) => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+
+ const handleDeposit = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'deposit', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ return (
+
+ {/* Hero Card */}
+
+
+
+
+
+ {translate('chainflipLending.dashboard.getStarted')}
+
+
+ {translate('chainflipLending.dashboard.depositFirstAsset')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Lending Markets Table */}
+
+
+ {/* Info Cards */}
+
+
+
+
+
+ )
+})
diff --git a/src/pages/ChainflipLending/components/LoanHealth.tsx b/src/pages/ChainflipLending/components/LoanHealth.tsx
new file mode 100644
index 00000000000..a6a0f31a62b
--- /dev/null
+++ b/src/pages/ChainflipLending/components/LoanHealth.tsx
@@ -0,0 +1,113 @@
+import { Card, CardBody, Flex, Icon, Stack, Text as CText } from '@chakra-ui/react'
+import { memo, useMemo } from 'react'
+import { TbHeartRateMonitor } from 'react-icons/tb'
+import { useTranslate } from 'react-polyglot'
+
+import { Amount } from '@/components/Amount/Amount'
+import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { Text } from '@/components/Text'
+import { bnOrZero } from '@/lib/bignumber/bignumber'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
+import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds'
+import { LtvGauge } from '@/pages/ChainflipLending/Pool/components/Borrow/LtvGauge'
+
+const DEFAULT_HARD_LIQUIDATION_LTV = 0.93
+
+const getLtvColor = (ltv: number, riskyThreshold: number, hardLiqThreshold: number): string => {
+ if (ltv >= hardLiqThreshold) return 'red.500'
+ if (ltv >= riskyThreshold) return 'yellow.500'
+ return 'green.500'
+}
+
+export const LoanHealth = memo(() => {
+ const translate = useTranslate()
+ const { totalCollateralFiat, totalBorrowedFiat } = useChainflipLoanAccount()
+ const { thresholds } = useChainflipLtvThresholds()
+
+ const hasLoans = useMemo(() => bnOrZero(totalBorrowedFiat).gt(0), [totalBorrowedFiat])
+
+ const currentLtv = useMemo(() => {
+ if (bnOrZero(totalCollateralFiat).isZero()) return 0
+ return bnOrZero(totalBorrowedFiat).div(totalCollateralFiat).toNumber()
+ }, [totalBorrowedFiat, totalCollateralFiat])
+
+ const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV
+ const riskyLtv = thresholds?.target ?? 0.8
+
+ const liquidationDistance = useMemo(() => {
+ if (hardLiquidationLtv === 0) return '0'
+ const liquidationCollateral = bnOrZero(totalBorrowedFiat).div(hardLiquidationLtv)
+ return bnOrZero(totalCollateralFiat).minus(liquidationCollateral).toFixed(2)
+ }, [totalCollateralFiat, totalBorrowedFiat, hardLiquidationLtv])
+
+ const liquidationDistancePercent = useMemo(() => {
+ if (bnOrZero(totalCollateralFiat).isZero()) return 0
+ return bnOrZero(liquidationDistance).div(totalCollateralFiat).times(100).toNumber()
+ }, [liquidationDistance, totalCollateralFiat])
+
+ const liquidationDistanceColor = useMemo(() => {
+ if (liquidationDistancePercent > 10) return 'green.500'
+ if (liquidationDistancePercent >= 5) return 'yellow.500'
+ return 'red.500'
+ }, [liquidationDistancePercent])
+
+ const ltvColor = useMemo(
+ () => getLtvColor(currentLtv, riskyLtv, hardLiquidationLtv),
+ [currentLtv, riskyLtv, hardLiquidationLtv],
+ )
+
+ const ltvDisplayPercent = useMemo(
+ () => (Math.min(Math.max(currentLtv, 0), 1) * 100).toFixed(1),
+ [currentLtv],
+ )
+
+ if (!hasLoans) return null
+
+ return (
+
+
+
+ {/* Header row: left = icon + label + current LTV, right = liquidation distance */}
+
+
+
+
+
+
+
+ {translate('chainflipLending.dashboard.currentLtv', {
+ ltv: `${ltvDisplayPercent}%`,
+ })}
+
+
+
+
+
+
+
+
+ {/* Multi-segment LTV gauge bar */}
+
+
+
+
+ )
+})
diff --git a/src/pages/ChainflipLending/components/Markets.tsx b/src/pages/ChainflipLending/components/Markets.tsx
index 7e559556420..7aca2a3effd 100644
--- a/src/pages/ChainflipLending/components/Markets.tsx
+++ b/src/pages/ChainflipLending/components/Markets.tsx
@@ -7,9 +7,14 @@ import {
SimpleGrid,
Skeleton,
Stack,
+ Tab,
+ TabList,
+ TabPanel,
+ TabPanels,
+ Tabs,
} from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
-import { useCallback, useMemo } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import { useNavigate } from 'react-router-dom'
@@ -21,14 +26,17 @@ import { AssetCell } from '@/components/StakingVaults/Cells'
import { Text } from '@/components/Text'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { permillToDecimal } from '@/lib/chainflip/utils'
+import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
import { ChainflipLendingHeader } from '@/pages/ChainflipLending/components/ChainflipLendingHeader'
-import { MyBalancesList } from '@/pages/ChainflipLending/components/MyBalances'
+import { Dashboard } from '@/pages/ChainflipLending/components/Dashboard'
import type { ChainflipLendingPoolWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { selectAssetById } from '@/state/slices/assetsSlice/selectors'
+import { useAppSelector } from '@/state/store'
const marketRowGrid: GridProps['gridTemplateColumns'] = {
base: 'minmax(150px, 1fr) repeat(1, minmax(40px, max-content))',
- md: '200px repeat(4, 1fr)',
+ md: '200px repeat(5, 1fr)',
}
const mobileDisplay = { base: 'none', md: 'flex' }
@@ -41,6 +49,9 @@ type MarketRowProps = {
}
const MarketRow = ({ pool, onViewMarket }: MarketRowProps) => {
+ const asset = useAppSelector(state =>
+ pool.assetId ? selectAssetById(state, pool.assetId) : undefined,
+ )
const handleClick = useCallback(() => {
if (pool.assetId) onViewMarket(pool.assetId)
}, [pool.assetId, onViewMarket])
@@ -76,13 +87,17 @@ const MarketRow = ({ pool, onViewMarket }: MarketRowProps) => {
height='auto'
color='text.base'
onClick={handleClick}
+ data-testid={`chainflip-lending-market-row-${asset?.symbol?.toLowerCase() ?? 'unknown'}`}
>
+
+
+
-
+
{
)
}
-export const Markets = () => {
+const MarketsTable = () => {
const translate = useTranslate()
const navigate = useNavigate()
const { pools, isLoading } = useChainflipLendingPools()
@@ -115,8 +130,6 @@ export const Markets = () => {
[navigate],
)
- const headerComponent = useMemo(() => , [])
-
const sortedPools = useMemo(
() => [...pools].sort((a, b) => bnOrZero(b.supplyApy).minus(a.supplyApy).toNumber()),
[pools],
@@ -134,59 +147,121 @@ export const Markets = () => {
)
}, [isLoading, sortedPools, handleViewMarket])
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {marketRows}
+
+
+ )
+}
+
+export const Markets = () => {
+ const translate = useTranslate()
+ const { accountId } = useChainflipLendingAccount()
+ const [tabIndex, setTabIndex] = useState(0)
+
+ const headerComponent = useMemo(() => , [tabIndex])
+
return (
-
-
-
-
-
-
-
-
-
-
+ {accountId ? (
+
+
+
+ {translate('chainflipLending.myDashboard')}
+
+
+ {translate('chainflipLending.markets')}
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {marketRows}
+ />
+
-
+ )}
)
diff --git a/src/pages/ChainflipLending/components/MyBalances.tsx b/src/pages/ChainflipLending/components/MyBalances.tsx
deleted file mode 100644
index c5238a2eaf8..00000000000
--- a/src/pages/ChainflipLending/components/MyBalances.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import type { GridProps } from '@chakra-ui/react'
-import { Button, Flex, SimpleGrid, Skeleton, Stack } from '@chakra-ui/react'
-import type { AssetId } from '@shapeshiftoss/caip'
-import { fromAssetId } from '@shapeshiftoss/caip'
-import { useCallback, useMemo } from 'react'
-import { useNavigate } from 'react-router-dom'
-
-import { Amount } from '@/components/Amount/Amount'
-import { AssetCell } from '@/components/StakingVaults/Cells'
-import { Text } from '@/components/Text'
-import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
-import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
-import type { ChainflipFreeBalanceWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
-import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
-import type {
- CollateralWithFiat,
- LoanWithFiat,
-} from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
-import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
-import type { ChainflipSupplyPositionWithFiat } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
-import { useChainflipSupplyPositions } from '@/pages/ChainflipLending/hooks/useChainflipSupplyPositions'
-import { selectAssetById } from '@/state/slices/assetsSlice/selectors'
-import { selectPortfolioCryptoBalanceByFilter } from '@/state/slices/common-selectors'
-import { selectAccountIdsByAccountNumberAndChainId } from '@/state/slices/portfolioSlice/selectors'
-import { useAppSelector } from '@/state/store'
-
-const balanceRowGrid: GridProps['gridTemplateColumns'] = {
- base: '1fr',
- md: '200px 1fr 1fr 1fr 1fr 1fr',
-}
-
-const mobileDisplay = { base: 'none', md: 'flex' }
-const mobilePadding = { base: 4, lg: 4, xl: 0 }
-const listMargin = { base: 0, lg: 0, xl: -4 }
-
-const LENDING_ASSET_IDS = Object.keys(CHAINFLIP_LENDING_ASSET_BY_ASSET_ID) as AssetId[]
-
-type BalanceRowProps = {
- assetId: AssetId
- accountNumber: number
- freeBalance: ChainflipFreeBalanceWithFiat | undefined
- supplyPosition: ChainflipSupplyPositionWithFiat | undefined
- collateral: CollateralWithFiat | undefined
- loan: LoanWithFiat | undefined
- onDeposit: (assetId: AssetId) => void
-}
-
-const BalanceRow = ({
- assetId,
- accountNumber,
- freeBalance,
- supplyPosition,
- collateral,
- loan,
- onDeposit,
-}: BalanceRowProps) => {
- const asset = useAppSelector(state => selectAssetById(state, assetId))
- const accountIdsByAccountNumberAndChainId = useAppSelector(
- selectAccountIdsByAccountNumberAndChainId,
- )
-
- const chainId = useMemo(() => fromAssetId(assetId).chainId, [assetId])
-
- const poolChainAccountId = useMemo(() => {
- const byChainId = accountIdsByAccountNumberAndChainId[accountNumber]
- return byChainId?.[chainId]?.[0]
- }, [accountIdsByAccountNumberAndChainId, accountNumber, chainId])
-
- const balanceFilter = useMemo(
- () => ({ assetId, accountId: poolChainAccountId ?? '' }),
- [assetId, poolChainAccountId],
- )
-
- const walletBalance = useAppSelector(state =>
- selectPortfolioCryptoBalanceByFilter(state, balanceFilter),
- )
-
- const walletBalancePrecision = useMemo(
- () => (poolChainAccountId ? walletBalance.toPrecision() : '0'),
- [walletBalance, poolChainAccountId],
- )
-
- const scBalancePrecision = useMemo(
- () => freeBalance?.balanceCryptoPrecision ?? '0',
- [freeBalance?.balanceCryptoPrecision],
- )
-
- const suppliedPrecision = useMemo(
- () => supplyPosition?.totalAmountCryptoPrecision ?? '0',
- [supplyPosition?.totalAmountCryptoPrecision],
- )
-
- const collateralPrecision = useMemo(
- () => collateral?.amountCryptoPrecision ?? '0',
- [collateral?.amountCryptoPrecision],
- )
-
- const borrowedPrecision = useMemo(
- () => loan?.principalAmountCryptoPrecision ?? '0',
- [loan?.principalAmountCryptoPrecision],
- )
-
- const handleClick = useCallback(() => {
- onDeposit(assetId)
- }, [assetId, onDeposit])
-
- const symbol = useMemo(() => asset?.symbol ?? '', [asset?.symbol])
-
- if (!asset) return null
-
- return (
-
- )
-}
-
-export const MyBalancesList = () => {
- const navigate = useNavigate()
- const { accountId, accountNumber } = useChainflipLendingAccount()
- const { freeBalances, isLoading } = useChainflipFreeBalances()
- const { supplyPositions, isLoading: isPositionsLoading } = useChainflipSupplyPositions()
- const { collateralWithFiat, loansWithFiat, isLoading: isLoanLoading } = useChainflipLoanAccount()
-
- const handleDeposit = useCallback(
- (assetId: AssetId) => {
- navigate(`/chainflip-lending/pool/${assetId}`)
- },
- [navigate],
- )
-
- const freeBalancesByAssetId = useMemo(
- () =>
- freeBalances.reduce>>(
- (acc, balance) => {
- if (balance.assetId) acc[balance.assetId] = balance
- return acc
- },
- {},
- ),
- [freeBalances],
- )
-
- const supplyPositionsByAssetId = useMemo(
- () =>
- supplyPositions.reduce>>(
- (acc, position) => {
- acc[position.assetId] = position
- return acc
- },
- {},
- ),
- [supplyPositions],
- )
-
- const collateralByAssetId = useMemo(
- () =>
- collateralWithFiat.reduce>>((acc, c) => {
- acc[c.assetId] = c
- return acc
- }, {}),
- [collateralWithFiat],
- )
-
- const loansByAssetId = useMemo(
- () =>
- loansWithFiat.reduce>>((acc, l) => {
- acc[l.assetId] = l
- return acc
- }, {}),
- [loansWithFiat],
- )
-
- const balanceRows = useMemo(() => {
- if (isLoading || isPositionsLoading || isLoanLoading) {
- return Array.from({ length: 5 }).map((_, i) => )
- }
-
- return LENDING_ASSET_IDS.map(assetId => (
-
- ))
- }, [
- accountNumber,
- isLoading,
- isPositionsLoading,
- isLoanLoading,
- freeBalancesByAssetId,
- supplyPositionsByAssetId,
- collateralByAssetId,
- loansByAssetId,
- handleDeposit,
- ])
-
- if (!accountId) return null
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {balanceRows}
-
- )
-}