diff --git a/.gitignore b/.gitignore
index 38f856104f5..79c806ad266 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,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..5b6ad6534af 100644
--- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml
+++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml
@@ -1,32 +1,88 @@
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.
+ Use a fresh session with no cached wallet state.
+ expected: >
+ Header shows 4 stat cards: Free Balance $0.00, Supplied $0.00, Collateral $0.00, Borrowed $0.00.
+ Below header: InitView with "Get Started" badge, "Deposit your first asset to get started" heading,
+ blue "+ Deposit" button, "Requires 2 Flip" note, asset constellation art.
+ Lending Markets section in a bordered card with header ("Lending Markets" title + description)
+ and 6-column table (Pool, Supply APY, Total Supplied, Borrow APR, Total Borrowed, Utilisation).
+ USDC row shows non-zero APY. No "Connect Wallet" button in the header stat area.
+ Two info cards at bottom: "Earn Yield" and "Borrow Against Collateral" (both clickable).
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.
+
+ # === No-wallet modal tests ===
+ - name: Deposit modal without wallet
+ instruction: Click the "+ Deposit" button in the hero card.
+ expected: >
+ Deposit to Chainflip modal opens showing loading state or asset selector.
+ The modal handles the no-wallet state internally.
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.
+
+ - name: Supply modal without wallet
+ instruction: Close deposit modal. Click the "Earn Yield" info card at the bottom of the page.
+ expected: >
+ Supply modal opens. Submit button shows "Connect Wallet" via ButtonWalletPredicate.
+ Pool APY and Current Position visible.
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: Borrow modal without wallet
+ instruction: Close supply modal. Click the "Borrow Against Collateral" info card.
+ expected: >
+ Borrow modal opens. Submit button shows "Connect Wallet" via ButtonWalletPredicate.
+ "Need more borrowing power? Add Collateral" link visible at bottom.
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.
+
+ # === Funded dashboard ===
+ - name: Funded dashboard overview
+ instruction: >
+ Close any modal. Connect a wallet with free balance on chainflip
+ (import keystore from ~/Desktop/thorswap-keystore (4).txt, password: testtttt).
+ Navigate to My Dashboard tab.
+ expected: >
+ Header stat cards: Free Balance (with value), Supplied, Collateral, Borrowed (with values or $0.00).
+ My Dashboard / Markets tabs visible, My Dashboard selected.
+ Free Balance section with Asset/Balance columns, listing assets with balances.
+ "+ Deposit" and "Withdraw" buttons in Free Balance header.
+ Borrowing Power gauge in sidebar. Next Steps card if applicable.
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: Dashboard sections
+ instruction: Scroll down to see all dashboard sections.
+ expected: >
+ Supplied section with amount and positions (or empty state).
+ Collateral section with USDC position showing amount.
+ Borrowed section (may show $0.00 if no active loans, or loans with borrow rate).
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.
+
+ # === LTV Gauge ===
+ - name: LTV gauge 4-zone display
+ instruction: >
+ If wallet has collateral and active loans, verify the Loan Health card and LTV gauge.
+ If no active loans (e.g. after voluntary liquidation), verify Loan Health card is hidden.
+ Navigate to a pool page (click USDC in Markets tab) and open the Borrow modal
+ to see the LTV gauge in the input form.
+ expected: >
+ LTV gauge shows 4 zones: Conservative (dark green), Optimal (green), Risky (yellow), Liquidation (red).
+ Legend below gauge shows all 4 zone labels.
+ If active loan: current LTV marker visible on the gauge.
+ Stats row below gauge: Total Collateral, Borrow Capacity, Est. Interest Rate.
+ "Need more borrowing power? Add Collateral" link below the Borrow button.
+ screenshot: true
+
+ # === Markets tab ===
+ - name: Markets tab
+ instruction: Close any open modal. Switch to the "Markets" tab.
+ expected: >
+ Markets table in bordered card with header section.
+ "Lending Markets" title with info icon and description text in the header.
+ 6-column table (Pool, Supply APY, Total Supplied, Borrow APR, Total Borrowed, Utilisation).
+ Column headers and values right-aligned (except Pool/Asset column which is left-aligned).
+ 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..2e5746644e9 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",
@@ -2501,7 +2518,17 @@
},
"noCollateral": "Add collateral first to borrow",
"minimumLoan": "Minimum loan: %{amount}",
- "availableTooltip": "Maximum amount you can borrow based on your collateral value and target LTV"
+ "availableTooltip": "Maximum amount you can borrow based on your collateral value and target LTV",
+ "needMorePower": "Need more borrowing power?",
+ "addCollateral": "Add Collateral"
+ },
+ "stats": {
+ "totalCollateral": "Total Collateral",
+ "totalCollateralTooltip": "Total value of all collateral deposited in your loan account.",
+ "borrowCapacity": "Borrow Capacity",
+ "borrowCapacityTooltip": "Remaining borrowing power based on your collateral and target LTV ratio.",
+ "estInterestRate": "Est. Interest Rate",
+ "estInterestRateTooltip": "Current annualized borrow rate for this pool. Rates adjust based on pool utilisation."
},
"totalSupplied": "Total Supplied",
"totalSuppliedTooltip": "Total value of assets supplied across all Chainflip lending pools.",
@@ -2529,6 +2556,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 +2572,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",
@@ -2570,6 +2601,9 @@
"noFreeBalance": "No free balance available to add as collateral",
"noRemovable": "Cannot remove collateral without reducing LTV below target"
},
+ "confirm": {
+ "destination": "Destination"
+ },
"depositToChainflip": "Deposit to Chainflip",
"depositDescription": "Deposit assets from your wallet to your Chainflip State Chain account to start lending.",
"freeBalanceDescription": "Available balance on Chainflip State Chain. Supply to pools to earn yield or use as collateral.",
@@ -2582,9 +2616,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 +2669,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 +2719,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 +2755,18 @@
"softLiquidation": "Soft Liquidation",
"hardLiquidation": "Hard Liquidation",
"safe": "Safe",
+ "risky": "Risky",
+ "liquidation": "Liquidation",
"warning": "Warning",
- "danger": "Danger"
+ "danger": "Danger",
+ "conservative": "Conservative",
+ "optimal": "Optimal"
},
"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",
@@ -2747,6 +2796,14 @@
"collateralHeader": "Collateral",
"borrowedHeader": "Borrowed",
"voluntaryLiquidation": {
+ "reduceDebt": "Reduce Debt",
+ "currentCollateral": "Current Collateral",
+ "outstandingDebt": "Outstanding Debt",
+ "currentLtv": "Current LTV",
+ "estimatedDuration": "Estimated Duration",
+ "estimatedDurationValue": "Varies by market",
+ "initiateWarningDetailed": "Collateral will be sold at near-market rates until your LTV is reduced. You can stop this process at any time. There is no fee unless LTV crosses a liquidation threshold.",
+ "confirmLiquidation": "Confirm Liquidation",
"initiateTitle": "Initiate Voluntary Liquidation",
"initiateWarning": "This will begin liquidating your entire position. Your collateral will be sold to repay all outstanding loans. This process cannot be undone once confirmed on-chain.",
"stopTitle": "Stop Voluntary Liquidation",
@@ -2755,17 +2812,84 @@
"confirmStop": "Stop Liquidation",
"executingTitle": "Processing...",
"executingDescription": "Your voluntary liquidation request is being submitted to the Chainflip State Chain.",
- "initiateSuccessTitle": "Liquidation Initiated",
- "initiateSuccessDescription": "Your voluntary liquidation has been initiated. Your position will be unwound automatically.",
+ "initiateSuccessTitle": "Voluntary liquidation started",
+ "initiateSuccessDescription": "Your collateral is now being sold via DCA at near-market rates to reduce your debt. You can stop these orders at any time from the dashboard.",
"stopSuccessTitle": "Liquidation Stopped",
"stopSuccessDescription": "Voluntary liquidation has been cancelled. Your position remains open.",
"errorTitle": "Transaction Failed",
"errorDescription": "The voluntary liquidation transaction failed. Please try again.",
"inProgress": "Voluntary liquidation in progress",
+ "viewDashboard": "View Dashboard",
+ "summaryStatus": "Status",
+ "summaryInProgress": "In progress",
+ "summaryMethod": "Method",
+ "summaryMethodValue": "DCA",
+ "summaryStopsWhen": "Stops when",
+ "summaryStopsWhenValue": "LTV at 0%",
+ "summaryUnsoldCollateral": "Unsold Collateral",
+ "summaryUnsoldCollateralValue": "Returned to your account",
"steps": {
"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.",
+ "voluntaryLiquidationActive": "Voluntary Liquidation Active",
+ "volLiqMethod": "Method",
+ "volLiqMethodValue": "DCA at near market rates",
+ "volLiqRemainingDebt": "Remaining Debt",
+ "volLiqCollateralSold": "Collateral sold",
+ "volLiqDescription": "Selling collateral to bring your position back to a healthy LTV. This process stops automatically once your LTV is restored, or you can stop it at any time.",
+ "volLiqStop": "Stop Liquidation"
}
},
"chart": {
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowConfirm.tsx
index 974b7a5f61c..76caf281c1a 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])
@@ -112,8 +119,16 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
})}
-
-
+
+
{translate('chainflipLending.borrow.borrowed')}
@@ -124,7 +139,7 @@ export const BorrowConfirm = memo(({ assetId }: BorrowConfirmProps) => {
fontSize='sm'
/>
-
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -258,24 +285,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 +328,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..cf3658ee576 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/BorrowInput.tsx
@@ -1,6 +1,7 @@
import { ArrowForwardIcon } from '@chakra-ui/icons'
import { Button, CardBody, CardFooter, Flex, HStack, Stack, VStack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
+import { ethChainId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import { BigAmount } from '@shapeshiftoss/utils'
import { useCallback, useMemo, useState } from 'react'
@@ -13,14 +14,18 @@ import { LtvGauge } from './LtvGauge'
import { Amount } from '@/components/Amount/Amount'
import { TradeAssetSelect } from '@/components/AssetSelection/AssetSelection'
+import { ButtonWalletPredicate } from '@/components/ButtonWalletPredicate/ButtonWalletPredicate'
import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
import { useModal } from '@/hooks/useModal/useModal'
+import { useWallet } from '@/hooks/useWallet/useWallet'
+import { useWalletSupportsChain } from '@/hooks/useWalletSupportsChain/useWalletSupportsChain'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
import { useChainflipBorrowMinimums } from '@/pages/ChainflipLending/hooks/useChainflipBorrowMinimums'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds'
import { useChainflipOraclePrice } from '@/pages/ChainflipLending/hooks/useChainflipOraclePrices'
@@ -33,8 +38,12 @@ type BorrowInputProps = {
onAssetChange: (assetId: AssetId) => void
}
+const DEFAULT_RISKY_LTV = 0.8
+
export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
const translate = useTranslate()
+ const wallet = useWallet().state.wallet
+ const walletSupportsEth = useWalletSupportsChain(ethChainId, wallet)
const {
number: { localeParts },
} = useLocaleFormatter()
@@ -50,6 +59,16 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
const { totalCollateralFiat, totalBorrowedFiat, loansWithFiat } = useChainflipLoanAccount()
const { thresholds } = useChainflipLtvThresholds()
const { minimumLoanAmountUsd, minimumUpdateLoanAmountUsd } = useChainflipBorrowMinimums()
+ const { pools } = useChainflipLendingPools()
+
+ const poolForAsset = useMemo(() => pools.find(p => p.assetId === assetId), [pools, assetId])
+
+ const borrowCapacityFiat = useMemo(() => {
+ if (!thresholds) return '0'
+ const maxBorrow = bnOrZero(totalCollateralFiat).times(thresholds.target)
+ const capacity = maxBorrow.minus(totalBorrowedFiat)
+ return capacity.gt(0) ? capacity.toFixed(2) : '0'
+ }, [totalCollateralFiat, totalBorrowedFiat, thresholds])
const hasExistingLoans = useMemo(() => loansWithFiat.length > 0, [loansWithFiat])
const effectiveMinimumUsd = useMemo(
@@ -100,6 +119,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)
@@ -113,6 +138,7 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
}, [assetIds, assets])
const buyAssetSearch = useModal('buyAssetSearch')
+ const chainflipLendingModal = useModal('chainflipLending')
const handleAssetClick = useCallback(() => {
buyAssetSearch.open({
@@ -135,6 +161,10 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
setInputValue(availableToBorrowCryptoPrecision)
}, [availableToBorrowCryptoPrecision])
+ const handleAddCollateral = useCallback(() => {
+ chainflipLendingModal.open({ mode: 'addCollateral', assetId })
+ }, [chainflipLendingModal, assetId])
+
const handleSubmit = useCallback(() => {
if (!asset) return
const baseUnit = BigAmount.fromPrecision({
@@ -256,7 +286,12 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
fontWeight='medium'
/>
-
+
)}
@@ -268,6 +303,50 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
/>
)}
+ {hasCollateral && (
+
+
+
+
+ {translate('chainflipLending.stats.totalCollateral')}
+
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.stats.borrowCapacity')}
+
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.stats.estInterestRate')}
+
+
+
+
+
+
+ )}
+
{effectiveMinimumUsd && isBelowMinimum && (
{translate('chainflipLending.borrow.minimumLoan', {
@@ -292,8 +371,9 @@ export const BorrowInput = ({ assetId, onAssetChange }: BorrowInputProps) => {
px={6}
py={4}
>
-
+
+
+ {translate('chainflipLending.borrow.needMorePower')}{' '}
+
+
)
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralConfirm.tsx
index 3723cbdd72d..bf14d914b15 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])
@@ -102,8 +109,16 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
)}
-
-
+
+
{translate(
isAddMode
@@ -118,7 +133,17 @@ export const CollateralConfirm = memo(({ assetId }: CollateralConfirmProps) => {
fontSize='sm'
/>
-
+ {!isAddMode && (
+
+
+ {translate('chainflipLending.confirm.destination')}
+
+
+ {translate('chainflipLending.freeBalance')}
+
+
+ )}
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -252,56 +289,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/CollateralInput.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralInput.tsx
index b81de64ba5d..d7d8fe36fb3 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/CollateralInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/CollateralInput.tsx
@@ -22,6 +22,7 @@ import { useToggle } from '@/hooks/useToggle/useToggle'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
import { useChainflipBorrowMinimums } from '@/pages/ChainflipLending/hooks/useChainflipBorrowMinimums'
+import { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
import { useChainflipLtvThresholds } from '@/pages/ChainflipLending/hooks/useChainflipLtvThresholds'
import { useChainflipOraclePrice } from '@/pages/ChainflipLending/hooks/useChainflipOraclePrices'
@@ -59,6 +60,16 @@ export const CollateralInput = ({ assetId, onAssetChange }: CollateralInputProps
const { totalCollateralFiat, totalBorrowedFiat } = useChainflipLoanAccount()
const { thresholds } = useChainflipLtvThresholds()
const { minimumUpdateCollateralAmountUsd } = useChainflipBorrowMinimums()
+ const { pools } = useChainflipLendingPools()
+
+ const poolForAsset = useMemo(() => pools.find(p => p.assetId === assetId), [pools, assetId])
+
+ const borrowCapacityFiat = useMemo(() => {
+ if (!thresholds) return '0'
+ const maxBorrow = bnOrZero(totalCollateralFiat).times(thresholds.target)
+ const capacity = maxBorrow.minus(totalBorrowedFiat)
+ return capacity.gt(0) ? capacity.toFixed(2) : '0'
+ }, [totalCollateralFiat, totalBorrowedFiat, thresholds])
const [isFiat, toggleIsFiat] = useToggle(false)
const [inputValue, setInputValue] = useState(savedCollateralAmount || '')
@@ -368,6 +379,50 @@ export const CollateralInput = ({ assetId, onAssetChange }: CollateralInputProps
)}
+ {hasActiveLoans && (
+
+
+
+
+ {translate('chainflipLending.stats.totalCollateral')}
+
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.stats.borrowCapacity')}
+
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.stats.estInterestRate')}
+
+
+
+
+
+
+ )}
+
{isBelowMinimum && minimumUpdateCollateralAmountUsd && (
{translate('chainflipLending.collateral.minimumAmount', {
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/LtvGauge.tsx
index 518e3e7e9e9..7ac2443e35f 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,215 @@ 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)}%`
+// Fallback thresholds when config hasn't loaded
+const DEFAULT_LOW_LTV = 0.5
+const DEFAULT_TARGET = 0.8
+const DEFAULT_SOFT_LIQUIDATION = 0.9
+const DEFAULT_TOPUP = 0.85
+const DEFAULT_HARD_LIQUIDATION = 0.93
-const DEFAULT_SOFT_LIQUIDATION_LTV = 0.8
-const DEFAULT_HARD_LIQUIDATION_LTV = 0.9
+const ltvToPercent = (ltv: number): number => Math.min(Math.max(ltv * 100, 0), 100)
-const getStatusColor = (
- ltv: number,
- softLiquidationLtv: number,
- hardLiquidationLtv: number,
-): string => {
- if (ltv >= hardLiquidationLtv) return 'red.500'
- if (ltv >= softLiquidationLtv) return 'yellow.500'
- return 'green.500'
+type Zone = {
+ labelKey: string
+ color: string
+ width: number
}
-const getStatusKey = (
+const getStatusColor = (
ltv: number,
- softLiquidationLtv: number,
- hardLiquidationLtv: number,
+ targetLtv: number,
+ softLiqLtv: number,
+ hardLiqLtv: number,
): string => {
- if (ltv >= hardLiquidationLtv) return 'chainflipLending.ltv.danger'
- if (ltv >= softLiquidationLtv) return 'chainflipLending.ltv.warning'
- return 'chainflipLending.ltv.safe'
+ if (ltv >= hardLiqLtv) return 'red.500'
+ if (ltv >= softLiqLtv) return 'yellow.500'
+ if (ltv >= targetLtv) return 'yellow.300'
+ return 'green.500'
}
export const LtvGauge = memo(({ currentLtv, projectedLtv }: LtvGaugeProps) => {
const translate = useTranslate()
const { thresholds } = useChainflipLtvThresholds()
- const softLiquidationLtv = thresholds?.softLiquidation ?? DEFAULT_SOFT_LIQUIDATION_LTV
- const hardLiquidationLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION_LTV
+ const lowLtv = thresholds?.lowLtv ?? DEFAULT_LOW_LTV
+ const targetLtv = thresholds?.target ?? DEFAULT_TARGET
+ const topupLtv = thresholds?.topup ?? DEFAULT_TOPUP
+ const softLiqLtv = thresholds?.softLiquidation ?? DEFAULT_SOFT_LIQUIDATION
+ const hardLiqLtv = thresholds?.hardLiquidation ?? DEFAULT_HARD_LIQUIDATION
- 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 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],
+ const projectedThumbPosition = useMemo(
+ () => (projectedLtv !== undefined ? ltvToPercent(projectedLtv) : undefined),
+ [projectedLtv],
)
- const thresholdMarkers = useMemo(() => {
- if (!thresholds) return []
- return [
+ // 4 zones based on protocol thresholds
+ const zones: Zone[] = useMemo(
+ () => [
{
- value: thresholds.target,
- labelKey: 'chainflipLending.ltv.target',
+ labelKey: 'chainflipLending.ltv.conservative',
+ color: 'green.700',
+ width: lowLtv * 100,
+ },
+ {
+ labelKey: 'chainflipLending.ltv.optimal',
color: 'green.500',
+ width: (targetLtv - lowLtv) * 100,
},
{
- value: thresholds.softLiquidation,
- labelKey: 'chainflipLending.ltv.softLiquidation',
+ labelKey: 'chainflipLending.ltv.risky',
color: 'yellow.500',
+ width: (softLiqLtv - targetLtv) * 100,
},
{
- value: thresholds.hardLiquidation,
- labelKey: 'chainflipLending.ltv.hardLiquidation',
+ labelKey: 'chainflipLending.ltv.liquidation',
color: 'red.500',
+ width: (1 - softLiqLtv) * 100,
},
- ]
- }, [thresholds])
+ ],
+ [lowLtv, targetLtv, softLiqLtv],
+ )
+
+ const skullPosition = useMemo(() => hardLiqLtv * 100, [hardLiqLtv])
+ const topupPosition = useMemo(() => topupLtv * 100, [topupLtv])
return (
-
-
+ {/* Bar track - 4 zone segments */}
+
-
+ {zones.map(zone => (
+
+ ))}
+
+
+ {/* Filled portion up to current LTV */}
+ = 100 ? 'full' : '0'}
+ overflow='hidden'
+ transition='width 0.3s ease'
+ >
+ 0 ? (100 / thumbPosition) * 100 : 100}%`}>
+ {zones.map(zone => (
+
+ ))}
+
+ {/* Topup threshold marker (subtle tick at 85%) */}
- {projectedLtv !== undefined && (
+ {/* Skull icon at hard liquidation boundary */}
+
+
+
+
+
+
+ {/* 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 - 4 zones */}
+
+ {zones.map(zone => (
+
+
+
+ {translate(zone.labelKey)}
+
+
+ ))}
+
)
})
diff --git a/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/RepayConfirm.tsx
index 59762893b31..8a0cd982f73 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])
@@ -96,8 +112,16 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
})}
-
-
+
+
{translate('chainflipLending.repay.repaid')}
@@ -115,7 +139,7 @@ export const RepayConfirm = memo(({ assetId }: RepayConfirmProps) => {
)}
-
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -249,50 +285,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/Borrow/VoluntaryLiquidationConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Borrow/VoluntaryLiquidationConfirm.tsx
index a9109e3edbb..4c7819937d3 100644
--- a/src/pages/ChainflipLending/Pool/components/Borrow/VoluntaryLiquidationConfirm.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Borrow/VoluntaryLiquidationConfirm.tsx
@@ -1,25 +1,31 @@
import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons'
-import { Button, CardBody, CardFooter, VStack } from '@chakra-ui/react'
+import { Box, Button, CardBody, CardFooter, Flex, HStack, VStack } from '@chakra-ui/react'
import { useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
+import { TbShieldHalf } from 'react-icons/tb'
import { useTranslate } from 'react-polyglot'
+import { useNavigate } from 'react-router-dom'
import { useVoluntaryLiquidationConfirmation } from './hooks/useVoluntaryLiquidationConfirmation'
import { useVoluntaryLiquidationSign } from './hooks/useVoluntaryLiquidationSign'
import { VoluntaryLiquidationMachineCtx } from './VoluntaryLiquidationMachineContext'
import { VoluntaryLiquidationStepper } from './VoluntaryLiquidationStepper'
+import { Amount } from '@/components/Amount/Amount'
import { CircularProgress } from '@/components/CircularProgress/CircularProgress'
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useModal } from '@/hooks/useModal/useModal'
import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
+import { useChainflipLoanAccount } from '@/pages/ChainflipLending/hooks/useChainflipLoanAccount'
import { reactQueries } from '@/react-queries'
export const VoluntaryLiquidationConfirm = memo(() => {
const translate = useTranslate()
+ const navigate = useNavigate()
const queryClient = useQueryClient()
const { scAccount } = useChainflipLendingAccount()
+ const { totalCollateralFiat, totalBorrowedFiat, loanAccount } = useChainflipLoanAccount()
const { close: closeModal } = useModal('chainflipLending')
@@ -38,6 +44,16 @@ export const VoluntaryLiquidationConfirm = memo(() => {
const isInitiate = useMemo(() => action === 'initiate', [action])
+ const currentLtvPercent = useMemo(() => {
+ if (!loanAccount?.ltv_ratio) return '0.0'
+ try {
+ const decimal = Number(BigInt(loanAccount.ltv_ratio)) / 1_000_000_000
+ return (decimal * 100).toFixed(1)
+ } catch {
+ return '0.0'
+ }
+ }, [loanAccount?.ltv_ratio])
+
const handleConfirm = useCallback(() => {
actorRef.send({ type: 'CONFIRM' })
}, [actorRef])
@@ -57,11 +73,118 @@ export const VoluntaryLiquidationConfirm = memo(() => {
closeModal()
}, [scAccount, queryClient, closeModal])
+ const handleViewDashboard = useCallback(async () => {
+ await handleDone()
+ navigate('/chainflip-lending')
+ }, [handleDone, navigate])
+
const handleBack = useCallback(() => {
actorRef.send({ type: 'BACK' })
}, [actorRef])
if (isSuccess) {
+ if (isInitiate) {
+ return (
+
+
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.initiateSuccessTitle')}
+
+
+ {translate('chainflipLending.voluntaryLiquidation.initiateSuccessDescription')}
+
+
+
+ }
+ >
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryStatus')}
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryInProgress')}
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryMethod')}
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryMethodValue')}
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryStopsWhen')}
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryStopsWhenValue')}
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.summaryUnsoldCollateral')}
+
+
+ {translate(
+ 'chainflipLending.voluntaryLiquidation.summaryUnsoldCollateralValue',
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ // Stop success - keep simple
return (
@@ -69,18 +192,10 @@ export const VoluntaryLiquidationConfirm = memo(() => {
- {translate(
- isInitiate
- ? 'chainflipLending.voluntaryLiquidation.initiateSuccessTitle'
- : 'chainflipLending.voluntaryLiquidation.stopSuccessTitle',
- )}
+ {translate('chainflipLending.voluntaryLiquidation.stopSuccessTitle')}
- {translate(
- isInitiate
- ? 'chainflipLending.voluntaryLiquidation.initiateSuccessDescription'
- : 'chainflipLending.voluntaryLiquidation.stopSuccessDescription',
- )}
+ {translate('chainflipLending.voluntaryLiquidation.stopSuccessDescription')}
@@ -204,25 +319,99 @@ export const VoluntaryLiquidationConfirm = memo(() => {
)
}
+ // Initiate confirm - redesigned with shield icon, info rows, and warning
+ if (isInitiate) {
+ return (
+
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.reduceDebt')}
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.currentCollateral')}
+
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.outstandingDebt')}
+
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.currentLtv')}
+
+
+ {currentLtvPercent}%
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.estimatedDuration')}
+
+
+ {translate('chainflipLending.voluntaryLiquidation.estimatedDurationValue')}
+
+
+
+
+ {translate('chainflipLending.voluntaryLiquidation.initiateWarningDetailed')}
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ // Stop confirm - keep existing style
return (
-
+
- {translate(
- isInitiate
- ? 'chainflipLending.voluntaryLiquidation.initiateTitle'
- : 'chainflipLending.voluntaryLiquidation.stopTitle',
- )}
+ {translate('chainflipLending.voluntaryLiquidation.stopTitle')}
- {translate(
- isInitiate
- ? 'chainflipLending.voluntaryLiquidation.initiateWarning'
- : 'chainflipLending.voluntaryLiquidation.stopDescription',
- )}
+ {translate('chainflipLending.voluntaryLiquidation.stopDescription')}
@@ -236,7 +425,7 @@ export const VoluntaryLiquidationConfirm = memo(() => {
py={4}
>
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -354,54 +381,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..3939e66bff0 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])
@@ -102,8 +109,16 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
})}
-
-
+
+
{translate('chainflipLending.egress.withdrawn')}
@@ -115,7 +130,7 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
/>
{egressTxRef && (
-
+
{translate('chainflipLending.egress.transactionId')}
@@ -133,7 +148,7 @@ export const EgressConfirm = memo(({ assetId }: EgressConfirmProps) => {
)}
)}
-
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -267,60 +294,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..fe39df0303c 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])
@@ -94,8 +110,16 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
})}
-
-
+
+
{translate('chainflipLending.supply.supplied')}
@@ -106,7 +130,17 @@ export const SupplyConfirm = memo(({ assetId }: SupplyConfirmProps) => {
fontSize='sm'
/>
-
+ {supplyApyPercent !== null && (
+
+
+ {translate('chainflipLending.supply.poolApy')}
+
+
+ {supplyApyPercent}%
+
+
+ )}
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -240,47 +286,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..ccc19665993 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 { ethChainId, usdcAssetId, usdtAssetId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import { BigAmount } from '@shapeshiftoss/utils'
import { useCallback, useMemo, useState } from 'react'
@@ -10,21 +11,27 @@ 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 { ButtonWalletPredicate } from '@/components/ButtonWalletPredicate/ButtonWalletPredicate'
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
import { useModal } from '@/hooks/useModal/useModal'
import { useToggle } from '@/hooks/useToggle/useToggle'
+import { useWallet } from '@/hooks/useWallet/useWallet'
+import { useWalletSupportsChain } from '@/hooks/useWalletSupportsChain/useWalletSupportsChain'
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
@@ -32,6 +39,8 @@ type SupplyInputProps = {
export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
const translate = useTranslate()
+ const wallet = useWallet().state.wallet
+ const walletSupportsEth = useWalletSupportsChain(ethChainId, wallet)
const {
number: { localeParts },
} = useLocaleFormatter()
@@ -79,6 +88,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 +168,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 +232,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 +241,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 +336,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 +345,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 && (
@@ -309,8 +465,9 @@ export const SupplyInput = ({ assetId, onAssetChange }: SupplyInputProps) => {
px={6}
py={4}
>
-
+
)
diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawConfirm.tsx
index 1034d8f6fa6..5ac58d9d227 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')
@@ -42,6 +44,8 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
const withdrawAmountCryptoPrecision = WithdrawMachineCtx.useSelector(
s => s.context.withdrawAmountCryptoPrecision,
)
+ const withdrawToWallet = WithdrawMachineCtx.useSelector(s => s.context.withdrawToWallet)
+ const withdrawAddress = WithdrawMachineCtx.useSelector(s => s.context.withdrawAddress)
const error = WithdrawMachineCtx.useSelector(s => s.context.error)
const isNativeWallet = WithdrawMachineCtx.useSelector(s => s.context.isNativeWallet)
const stepConfirmed = WithdrawMachineCtx.useSelector(s => s.context.stepConfirmed)
@@ -71,6 +75,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])
@@ -93,8 +102,16 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
})}
-
-
+
+
{translate('chainflipLending.withdraw.withdrawn')}
@@ -105,7 +122,23 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
fontSize='sm'
/>
-
+
+
+ {translate('chainflipLending.withdraw.destination')}
+
+
+ {withdrawToWallet && withdrawAddress
+ ? withdrawAddress
+ : translate('chainflipLending.freeBalance')}
+
+
+
{
px={6}
py={4}
>
-
+
+
+
+
)
@@ -239,47 +284,75 @@ export const WithdrawConfirm = memo(({ assetId }: WithdrawConfirmProps) => {
return (
-
-
-
-
-
-
-
- {translate('chainflipLending.withdraw.confirmTitle')}
-
-
+
+
+
+
+
+
+
+
+ {translate('chainflipLending.withdraw.amount')}
+
+
+
+ {translate('chainflipLending.withdraw.destination')}
+
+
+ {withdrawToWallet && withdrawAddress
+ ? withdrawAddress
+ : translate('chainflipLending.freeBalance')}
+
+
+
-
)
diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawInput.tsx b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawInput.tsx
index fb2682a7ccd..98b9d0070f9 100644
--- a/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawInput.tsx
+++ b/src/pages/ChainflipLending/Pool/components/Withdraw/WithdrawInput.tsx
@@ -1,33 +1,58 @@
-import { Button, CardBody, CardFooter, Flex, Stack, VStack } from '@chakra-ui/react'
-import type { AssetId } from '@shapeshiftoss/caip'
-import { ethChainId } from '@shapeshiftoss/caip'
+import {
+ Button,
+ CardBody,
+ CardFooter,
+ Checkbox,
+ Flex,
+ FormControl,
+ FormHelperText,
+ HStack,
+ Input,
+ Stack,
+ VStack,
+} from '@chakra-ui/react'
+import type { AccountId, AssetId } from '@shapeshiftoss/caip'
+import { ethChainId, fromAccountId, fromAssetId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import { BigAmount } from '@shapeshiftoss/utils'
import { useCallback, useMemo, useState } from 'react'
+import { useForm } from 'react-hook-form'
import type { NumberFormatValues } from 'react-number-format'
import { NumericFormat } from 'react-number-format'
import { useTranslate } from 'react-polyglot'
import { WithdrawMachineCtx } from './WithdrawMachineContext'
+import { AccountDropdown } from '@/components/AccountDropdown/AccountDropdown'
import { Amount } from '@/components/Amount/Amount'
import { TradeAssetSelect } from '@/components/AssetSelection/AssetSelection'
import { ButtonWalletPredicate } from '@/components/ButtonWalletPredicate/ButtonWalletPredicate'
import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
+import { InlineCopyButton } from '@/components/InlineCopyButton'
import { SlideTransition } from '@/components/SlideTransition'
import { RawText } from '@/components/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
import { useModal } from '@/hooks/useModal/useModal'
import { useWallet } from '@/hooks/useWallet/useWallet'
import { useWalletSupportsChain } from '@/hooks/useWalletSupportsChain/useWalletSupportsChain'
+import { validateAddress } from '@/lib/address/validation'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_BY_ASSET_ID } from '@/lib/chainflip/constants'
+import { useChainflipLendingAccount } from '@/pages/ChainflipLending/ChainflipLendingAccountContext'
import { useChainflipMinimumSupply } from '@/pages/ChainflipLending/hooks/useChainflipMinimumSupply'
import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors'
+import { selectAccountIdsByAccountNumberAndChainId } from '@/state/slices/portfolioSlice/selectors'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
import { selectAssetById, selectAssets } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
+const dropdownBoxProps = { width: 'full', p: 0, m: 0 }
+const dropdownButtonProps = { width: 'full', variant: 'solid', height: '40px', px: 4 }
+
+type AddressFormValues = {
+ manualAddress: string
+}
+
type WithdrawInputProps = {
assetId: AssetId
onAssetChange: (assetId: AssetId) => void
@@ -43,6 +68,10 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
const asset = useAppSelector(state => selectAssetById(state, assetId))
const marketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId))
+ const { accountNumber } = useChainflipLendingAccount()
+ const accountIdsByAccountNumberAndChainId = useAppSelector(
+ selectAccountIdsByAccountNumberAndChainId,
+ )
const actorRef = WithdrawMachineCtx.useActorRef()
const supplyPositionCryptoBaseUnit = WithdrawMachineCtx.useSelector(
@@ -55,6 +84,36 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
const [withdrawAmountCryptoPrecision, setWithdrawAmountCryptoPrecision] = useState(
savedWithdrawAmount || '',
)
+ const [withdrawToWallet, setWithdrawToWallet] = useState(false)
+
+ const chainId = useMemo(() => fromAssetId(assetId).chainId, [assetId])
+ const walletSupportsAssetChain = useWalletSupportsChain(chainId, wallet)
+
+ const poolChainAccountId = useMemo(() => {
+ const byChainId = accountIdsByAccountNumberAndChainId[accountNumber]
+ return byChainId?.[chainId]?.[0]
+ }, [accountIdsByAccountNumberAndChainId, accountNumber, chainId])
+
+ const defaultAddress = useMemo(
+ () => (poolChainAccountId ? fromAccountId(poolChainAccountId).account : ''),
+ [poolChainAccountId],
+ )
+
+ const [destinationAddress, setDestinationAddress] = useState(defaultAddress)
+ const [defaultAccountId, setDefaultAccountId] = useState(
+ poolChainAccountId,
+ )
+ const [isCustomAddress, setIsCustomAddress] = useState(!walletSupportsAssetChain)
+
+ const {
+ register,
+ formState: { errors },
+ setValue,
+ trigger,
+ } = useForm({
+ mode: 'onChange',
+ reValidateMode: 'onChange',
+ })
const availableCryptoPrecision = useMemo(
() =>
@@ -117,6 +176,58 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
setWithdrawAmountCryptoPrecision(availableCryptoPrecision)
}, [availableCryptoPrecision])
+ const handleWithdrawToWalletChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const checked = e.target.checked
+ setWithdrawToWallet(checked)
+ if (checked) {
+ const showCustom = !walletSupportsAssetChain
+ setDestinationAddress(showCustom ? '' : defaultAddress)
+ setIsCustomAddress(showCustom)
+ }
+ },
+ [defaultAddress, walletSupportsAssetChain],
+ )
+
+ const handleAccountChange = useCallback((newAccountId: string) => {
+ const address = fromAccountId(newAccountId).account
+ setDefaultAccountId(newAccountId)
+ setDestinationAddress(address)
+ }, [])
+
+ const handleToggleCustomAddress = useCallback(() => {
+ if (!walletSupportsAssetChain) return
+ const walletAddress = defaultAccountId ? fromAccountId(defaultAccountId).account : ''
+ setDestinationAddress(isCustomAddress ? walletAddress : '')
+ setIsCustomAddress(prev => !prev)
+ }, [walletSupportsAssetChain, isCustomAddress, defaultAccountId])
+
+ const validateChainAddress = useCallback(
+ async (address: string) => {
+ if (!address) {
+ setDestinationAddress('')
+ return true
+ }
+ const isValid = await validateAddress({ maybeAddress: address, chainId })
+ if (!isValid) {
+ setDestinationAddress('')
+ return translate('common.invalidAddress')
+ }
+ setDestinationAddress(address)
+ return true
+ },
+ [chainId, translate],
+ )
+
+ const handleManualInputChange = useCallback(
+ async (e: React.ChangeEvent) => {
+ const newValue = e.target.value
+ setValue('manualAddress', newValue, { shouldValidate: true })
+ await trigger('manualAddress')
+ },
+ [setValue, trigger],
+ )
+
const isFullWithdrawal = useMemo(
() =>
isFullWithdrawalOnly ||
@@ -141,6 +252,8 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
const handleSubmit = useCallback(() => {
if (!asset) return
+ const withdrawAddress = withdrawToWallet ? destinationAddress : ''
+ if (withdrawToWallet && !withdrawAddress) return
const amountPrecision = isFullWithdrawalOnly
? availableCryptoPrecision
: withdrawAmountCryptoPrecision
@@ -152,6 +265,8 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
type: 'SUBMIT',
withdrawAmountCryptoPrecision: amountPrecision,
withdrawAmountCryptoBaseUnit,
+ withdrawAddress,
+ withdrawToWallet,
isFullWithdrawal,
})
}, [
@@ -160,16 +275,27 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
availableCryptoPrecision,
isFullWithdrawalOnly,
asset,
+ destinationAddress,
+ withdrawToWallet,
isFullWithdrawal,
])
+ const isValidWallet = useMemo(
+ () =>
+ Boolean(
+ walletSupportsEth && (!withdrawToWallet || walletSupportsAssetChain || isCustomAddress),
+ ),
+ [walletSupportsEth, withdrawToWallet, walletSupportsAssetChain, isCustomAddress],
+ )
+
const isSubmitDisabled = useMemo(
() =>
isMinSupplyLoading ||
(!isFullWithdrawalOnly && bnOrZero(withdrawAmountCryptoPrecision).isZero()) ||
bnOrZero(withdrawAmountCryptoPrecision).gt(availableCryptoPrecision) ||
isBelowMinimum ||
- isRemainingBelowMinimum,
+ isRemainingBelowMinimum ||
+ (withdrawToWallet && !destinationAddress.trim()),
[
isMinSupplyLoading,
withdrawAmountCryptoPrecision,
@@ -177,6 +303,8 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
isFullWithdrawalOnly,
isBelowMinimum,
isRemainingBelowMinimum,
+ withdrawToWallet,
+ destinationAddress,
],
)
@@ -287,9 +415,70 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
)}
-
- {translate('chainflipLending.withdraw.freeBalanceExplainer')}
-
+
+
+ {translate('chainflipLending.withdraw.freeBalanceExplainer')}
+
+
+
+ {translate('chainflipLending.withdraw.alsoWithdrawToWallet')}
+
+
+ {withdrawToWallet && (
+
+
+
+ {translate('chainflipLending.withdraw.destinationAddress')}
+
+ {walletSupportsAssetChain && (
+
+ )}
+
+ {isCustomAddress ? (
+
+ ) : (
+
+
+
+ )}
+ {errors.manualAddress && (
+
+ {errors.manualAddress.message as string}
+
+ )}
+
+ )}
+
>
) : (
@@ -309,7 +498,7 @@ export const WithdrawInput = ({ assetId, onAssetChange }: WithdrawInputProps) =>
>
{
lastUsedNonce,
isNativeWallet,
stepConfirmed,
+ withdrawToWallet,
+ withdrawAddress,
} = WithdrawMachineCtx.useSelector(s => ({
assetId: s.context.assetId,
withdrawAmountCryptoBaseUnit: s.context.withdrawAmountCryptoBaseUnit,
@@ -25,6 +27,8 @@ export const useWithdrawSign = () => {
lastUsedNonce: s.context.lastUsedNonce,
isNativeWallet: s.context.isNativeWallet,
stepConfirmed: s.context.stepConfirmed,
+ withdrawToWallet: s.context.withdrawToWallet,
+ withdrawAddress: s.context.withdrawAddress,
}))
const wallet = useWallet().state.wallet
const { accountId, scAccount } = useChainflipLendingAccount()
@@ -45,10 +49,22 @@ export const useWithdrawSign = () => {
if (!scAccount) throw new Error('State Chain account not derived')
if (!cfAsset) throw new Error(`Unsupported asset: ${assetId}`)
- const encodedCall = encodeRemoveLenderFunds(
+ const removeCall = encodeRemoveLenderFunds(
cfAsset,
isFullWithdrawal ? null : withdrawAmountCryptoBaseUnit,
)
+
+ let encodedCall: string
+ if (withdrawToWallet && withdrawAddress) {
+ const egressCall = encodeWithdrawAsset(withdrawAmountCryptoBaseUnit, cfAsset, {
+ chain: cfAsset.chain,
+ address: withdrawAddress,
+ })
+ encodedCall = encodeBatch([removeCall, egressCall])
+ } else {
+ encodedCall = removeCall
+ }
+
const nonceOrAccount = lastUsedNonce !== undefined ? lastUsedNonce + 1 : scAccount
const { txHash, nonce } = await signAndSubmit({
@@ -81,5 +97,7 @@ export const useWithdrawSign = () => {
isFullWithdrawal,
isNativeWallet,
stepConfirmed,
+ withdrawToWallet,
+ withdrawAddress,
])
}
diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.test.ts b/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.test.ts
index 7ca42959021..489c194d8e4 100644
--- a/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.test.ts
+++ b/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.test.ts
@@ -11,6 +11,8 @@ const defaultSubmitEvent = {
withdrawAmountCryptoPrecision: '100.5',
withdrawAmountCryptoBaseUnit: '100500000',
isFullWithdrawal: false,
+ withdrawToWallet: false,
+ withdrawAddress: '',
}
const createTestActor = (overrides?: Partial<{ isNativeWallet: boolean }>) => {
diff --git a/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.ts b/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.ts
index 575446dea94..867b5d02fcf 100644
--- a/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.ts
+++ b/src/pages/ChainflipLending/Pool/components/Withdraw/withdrawMachine.ts
@@ -10,6 +10,8 @@ type WithdrawMachineContext = {
withdrawAmountCryptoBaseUnit: string
supplyPositionCryptoBaseUnit: string
isFullWithdrawal: boolean
+ withdrawToWallet: boolean
+ withdrawAddress: string
stepConfirmed: boolean
txHash: string | null
lastUsedNonce: number | undefined
@@ -28,6 +30,8 @@ type WithdrawMachineEvent =
withdrawAmountCryptoPrecision: string
withdrawAmountCryptoBaseUnit: string
isFullWithdrawal: boolean
+ withdrawToWallet: boolean
+ withdrawAddress: string
}
| { type: 'CONFIRM' }
| { type: 'BACK' }
@@ -62,6 +66,14 @@ export const withdrawMachine = setup({
assertEvent(event, 'SUBMIT')
return event.isFullWithdrawal
},
+ withdrawToWallet: ({ event }) => {
+ assertEvent(event, 'SUBMIT')
+ return event.withdrawToWallet
+ },
+ withdrawAddress: ({ event }) => {
+ assertEvent(event, 'SUBMIT')
+ return event.withdrawAddress
+ },
}),
syncSupplyPosition: assign({
supplyPositionCryptoBaseUnit: ({ event }) => {
@@ -92,6 +104,8 @@ export const withdrawMachine = setup({
withdrawAmountCryptoPrecision: '',
withdrawAmountCryptoBaseUnit: '0',
isFullWithdrawal: false,
+ withdrawToWallet: false,
+ withdrawAddress: '',
lastUsedNonce: undefined,
txHash: null,
error: null,
@@ -108,6 +122,8 @@ export const withdrawMachine = setup({
withdrawAmountCryptoBaseUnit: '0',
supplyPositionCryptoBaseUnit: '0',
isFullWithdrawal: false,
+ withdrawToWallet: false,
+ withdrawAddress: '',
stepConfirmed: false,
txHash: null,
lastUsedNonce: undefined,
diff --git a/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx b/src/pages/ChainflipLending/components/ChainflipLendingHeader.tsx
index d1d16a66570..557a0943ffe 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 { 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'
@@ -10,31 +10,80 @@ import { Display } from '@/components/Display'
import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
import { PageBackButton, PageHeader } from '@/components/Layout/Header/PageHeader'
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 { useChainflipLendingPools } from '@/pages/ChainflipLending/hooks/useChainflipLendingPools'
+import { useChainflipFreeBalances } from '@/pages/ChainflipLending/hooks/useChainflipFreeBalances'
+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 (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const ChainflipLendingHeader = memo(() => {
const translate = useTranslate()
const navigate = useNavigate()
- const { dispatch: walletDispatch } = useWallet()
const { accountId, setAccountId } = useChainflipLendingAccount()
const handleBack = useCallback(() => {
navigate('/explore')
}, [navigate])
- const handleConnectWallet = useCallback(
- () => walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }),
- [walletDispatch],
+ 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 { totalSuppliedFiat, availableLiquidityFiat, totalBorrowedFiat, isLoading } =
- useChainflipLendingPools()
+ const {
+ totalCollateralFiat,
+ totalBorrowedFiat: userBorrowedFiat,
+ isLoading: isLoanLoading,
+ } = useChainflipLoanAccount()
+
+ const isUserDataLoading =
+ accountId !== undefined && (isFreeBalancesLoading || isPositionsLoading || isLoanLoading)
return (
<>
@@ -49,74 +98,58 @@ export const ChainflipLendingHeader = () => {
-
+
{translate('navBar.chainflipLending')}
-
+ {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..eb82aa1d439
--- /dev/null
+++ b/src/pages/ChainflipLending/components/DashboardSidebar.tsx
@@ -0,0 +1,404 @@
+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 { RawText, 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}
+
+
+
+ )
+})
+
+const VoluntaryLiquidationActiveCard = memo(() => {
+ const translate = useTranslate()
+ const { loanAccount, totalBorrowedFiat, totalCollateralFiat } = useChainflipLoanAccount()
+ const chainflipLendingModal = useModal('chainflipLending')
+
+ const isVoluntaryLiquidationActive = useMemo(() => {
+ if (!loanAccount?.liquidation_status) return false
+ const status = loanAccount.liquidation_status as Record
+ return status.liquidation_type === 'Voluntary'
+ }, [loanAccount?.liquidation_status])
+
+ const handleStopLiquidation = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId)
+ chainflipLendingModal.open({ mode: 'voluntaryLiquidation', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ if (!isVoluntaryLiquidationActive) return null
+
+ return (
+
+
+
+
+
+
+ {translate('chainflipLending.dashboard.voluntaryLiquidationActive')}
+
+
+
+
+
+ {translate('chainflipLending.dashboard.volLiqMethod')}
+
+
+ {translate('chainflipLending.dashboard.volLiqMethodValue')}
+
+
+
+
+ {translate('chainflipLending.dashboard.volLiqRemainingDebt')}
+
+
+
+
+
+ {translate('chainflipLending.dashboard.volLiqCollateralSold')}
+
+
+
+
+
+ {translate('chainflipLending.dashboard.volLiqDescription')}
+
+
+
+
+
+ )
+})
+
+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..a6f1ef99ec7
--- /dev/null
+++ b/src/pages/ChainflipLending/components/InitView.tsx
@@ -0,0 +1,619 @@
+import {
+ Badge,
+ Box,
+ Button,
+ Card,
+ CardBody,
+ Center,
+ CircularProgress,
+ Flex,
+ 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 { RawText, 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')}
+
+
+
+
+ {translate('chainflipLending.dashboard.lendingMarketsDescription')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {marketRows}
+
+ )
+})
+
+type InfoCardProps = {
+ titleKey: string
+ descriptionKey: string
+ accentColor: string
+ onClick?: () => void
+ 'data-testid'?: string
+}
+
+const InfoCard = memo(
+ ({ titleKey, descriptionKey, accentColor, onClick, '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 */}
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ )
+ },
+)
+
+type InitViewProps = {
+ onCtaClick?: () => void
+ ctaLabel?: string
+ 'data-testid'?: string
+}
+
+export const InitView = memo(({ onCtaClick, ctaLabel, 'data-testid': testId }: InitViewProps) => {
+ const translate = useTranslate()
+ const chainflipLendingModal = useModal('chainflipLending')
+
+ const handleDeposit = useCallback(() => {
+ if (onCtaClick) {
+ onCtaClick()
+ return
+ }
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'deposit', assetId: firstAssetId })
+ }, [chainflipLendingModal, onCtaClick])
+
+ const handleSupply = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'supply', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ const handleBorrow = useCallback(() => {
+ const firstAssetId = LENDING_ASSET_IDS[0]
+ if (firstAssetId) chainflipLendingModal.open({ mode: 'borrow', assetId: firstAssetId })
+ }, [chainflipLendingModal])
+
+ return (
+
+ {/* Hero Card */}
+
+
+
+
+
+
+ {translate('chainflipLending.dashboard.getStarted')}
+
+
+ {translate('chainflipLending.dashboard.depositFirstAsset')}
+
+
+ {translate('chainflipLending.dashboard.depositFirstAssetDescription')}
+
+
+
+
+
+ {translate('chainflipLending.dashboard.requiresFlip')}
+
+
+
+
+
+
+
+
+ {/* 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..03d6c11c958 100644
--- a/src/pages/ChainflipLending/components/Markets.tsx
+++ b/src/pages/ChainflipLending/components/Markets.tsx
@@ -1,5 +1,6 @@
import type { GridProps } from '@chakra-ui/react'
import {
+ Box,
Button,
CircularProgress,
Flex,
@@ -7,9 +8,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'
@@ -18,22 +24,24 @@ import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
import { Main } from '@/components/Layout/Main'
import { SEO } from '@/components/Layout/Seo'
import { AssetCell } from '@/components/StakingVaults/Cells'
-import { Text } from '@/components/Text'
+import { RawText, 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 { InitView } from '@/pages/ChainflipLending/components/InitView'
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' }
-const mobilePadding = { base: 4, lg: 4, xl: 0 }
-const listMargin = { base: 0, lg: 0, xl: -4 }
type MarketRowProps = {
pool: ChainflipLendingPoolWithFiat
@@ -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,20 +87,24 @@ 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],
@@ -135,58 +148,121 @@ export const Markets = () => {
}, [isLoading, sortedPools, handleViewMarket])
return (
-
-
-
-
-
-
+
+
+
+
+
+ {translate('chainflipLending.dashboard.lendingMarkets')}
+
+
+
+
+ {translate('chainflipLending.dashboard.lendingMarketsDescription')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {marketRows}
+
+ )
+}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {marketRows}
-
-
+export const Markets = () => {
+ const translate = useTranslate()
+ const { accountId } = useChainflipLendingAccount()
+ const [tabIndex, setTabIndex] = useState(0)
+
+ const headerComponent = useMemo(() => , [])
+
+ return (
+
+
+
+ {accountId ? (
+
+
+
+ {translate('chainflipLending.myDashboard')}
+
+
+ {translate('chainflipLending.markets')}
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
)
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}
-
- )
-}
diff --git a/src/pages/ChainflipLending/hooks/useChainflipOraclePrices.ts b/src/pages/ChainflipLending/hooks/useChainflipOraclePrices.ts
index e91969a216e..a3a4554991f 100644
--- a/src/pages/ChainflipLending/hooks/useChainflipOraclePrices.ts
+++ b/src/pages/ChainflipLending/hooks/useChainflipOraclePrices.ts
@@ -2,6 +2,7 @@ import type { AssetId } from '@shapeshiftoss/caip'
import { useQuery } from '@tanstack/react-query'
import BigNumber from 'bignumber.js'
import { useMemo } from 'react'
+import { shallowEqual } from 'react-redux'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET } from '@/lib/chainflip/constants'
@@ -43,8 +44,9 @@ export const useChainflipOraclePrices = () => {
.filter((id): id is AssetId => Boolean(id))
}, [oraclePrices])
- const assetPrecisions = useAppSelector(state =>
- assetIds.map(id => selectAssetById(state, id)?.precision ?? 0),
+ const assetPrecisions = useAppSelector(
+ state => assetIds.map(id => selectAssetById(state, id)?.precision ?? 0),
+ shallowEqual,
)
const oraclePriceByAssetId = useMemo(() => {
diff --git a/src/pages/ChainflipLending/hooks/useChainflipSupplyPositions.ts b/src/pages/ChainflipLending/hooks/useChainflipSupplyPositions.ts
index a9f61ebbb15..5138a43a4fc 100644
--- a/src/pages/ChainflipLending/hooks/useChainflipSupplyPositions.ts
+++ b/src/pages/ChainflipLending/hooks/useChainflipSupplyPositions.ts
@@ -1,5 +1,6 @@
import type { AssetId } from '@shapeshiftoss/caip'
import { useMemo } from 'react'
+import { shallowEqual } from 'react-redux'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET } from '@/lib/chainflip/constants'
@@ -40,13 +41,15 @@ export const useChainflipSupplyPositions = () => {
[accountInfo?.lending_positions],
)
- const positionAssetData = useAppSelector(state =>
- lendingPositions.map(position => {
- const assetId = CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET[position.asset]
- if (!assetId) return { assetId: undefined, precision: 0 }
- const asset = selectAssetById(state, assetId)
- return { assetId, precision: asset?.precision ?? 0 }
- }),
+ const positionAssetData = useAppSelector(
+ state =>
+ lendingPositions.map(position => {
+ const assetId = CHAINFLIP_LENDING_ASSET_IDS_BY_ASSET[position.asset]
+ if (!assetId) return { assetId: undefined, precision: 0 }
+ const asset = selectAssetById(state, assetId)
+ return { assetId, precision: asset?.precision ?? 0 }
+ }),
+ shallowEqual,
)
const supplyPositions: ChainflipSupplyPositionWithFiat[] = useMemo(() => {