Morpho Solana Frontend Builder
Build a complete, production-ready frontend for the Morpho Blue lending protocol on Solana with all 26 instructions implemented.
Overview
This skill guides you through building a DeFi lending interface that covers:
- Core Lending: Supply, withdraw, borrow, repay, collateral management
- Flash Loans: All 3 flash loan modes (single-instruction, two-step start/end)
- Liquidations: Unhealthy position scanner and liquidation interface
- Authorization: Time-based delegation system with revocation
- Admin Panel: Protocol management, whitelisting, fee controls
- Position Management: Auto-create positions, close empty positions
Tech Stack
Core Framework:
├── Next.js 14 (App Router)
├── TypeScript
├── TailwindCSS + shadcn/ui
└── Framer Motion
Solana Integration:
├── @solana/web3.js
├── @solana/wallet-adapter-react (Jupiter)
├── @coral-xyz/anchor
└── @solana/spl-token
State Management:
├── Zustand (global state)
├── TanStack Query (async state)
└── WebSocket subscriptions
UI Components:
├── Recharts (analytics)
├── Radix UI (primitives)
└── shadcn/ui (pre-built components)
Project Structure
app/
├── (user)/ # User routes (no auth required)
│ ├── markets/
│ │ ├── page.tsx # Markets explorer
│ │ └── [marketId]/
│ │ └── page.tsx # Market detail
│ ├── dashboard/page.tsx # User positions
│ ├── liquidations/page.tsx # Liquidation opportunities
│ ├── flash-loans/page.tsx # Flash loan interface
│ ├── settings/page.tsx # User settings
│ └── page.tsx # Landing page
├── (admin)/ # Admin routes (owner only)
│ └── admin/
│ ├── layout.tsx # Admin auth wrapper
│ ├── page.tsx # Admin dashboard
│ ├── protocol/page.tsx # Protocol settings
│ ├── markets/page.tsx # Market management
│ └── whitelist/page.tsx # LLTV/IRM whitelist
└── layout.tsx # Root layout
components/
├── wallet/
│ ├── WalletProvider.tsx # Jupiter wallet setup
│ ├── WalletButton.tsx # Connect button
│ └── WalletDropdown.tsx # Balance, disconnect
├── market/
│ ├── MarketCard.tsx # Market list item
│ ├── MarketStats.tsx # TVL, APY display
│ └── tabs/
│ ├── SupplyTab.tsx # supply()
│ ├── WithdrawTab.tsx # withdraw()
│ ├── CollateralTab.tsx # supply_collateral() + withdraw_collateral()
│ ├── BorrowTab.tsx # borrow()
│ ├── RepayTab.tsx # repay()
│ ├── LiquidateTab.tsx # liquidate()
│ ├── FlashLoanTab.tsx # flash_loan() variants
│ └── AuthorizationTab.tsx # set_authorization()
├── position/
│ ├── PositionCard.tsx # Position display
│ ├── HealthFactor.tsx # Health visualization
│ └── PositionManager.tsx # create_position(), close_position()
├── admin/
│ ├── ProtocolControls.tsx # Pause, ownership
│ ├── WhitelistManager.tsx # enable_lltv(), enable_irm()
│ ├── MarketControls.tsx # set_fee(), set_market_paused()
│ └── FeeClaimButton.tsx # claim_fees()
├── shared/
│ ├── TokenSelector.tsx # Token dropdown
│ ├── TransactionModal.tsx # TX signing flow
│ ├── HealthFactorBar.tsx # Visual health
│ └── APYBadge.tsx # Supply/borrow APY
└── layout/
├── Header.tsx # Nav + wallet
├── Sidebar.tsx # Navigation
└── Footer.tsx
lib/
├── anchor/
│ ├── idl.ts # Morpho program IDL
│ ├── client.ts # Anchor Program instance
│ └── instructions/
│ ├── supply.ts # Supply instruction builders
│ ├── borrow.ts # Borrow instruction builders
│ ├── liquidate.ts # Liquidation instruction
│ ├── flashLoan.ts # Flash loan instructions
│ ├── admin.ts # Admin instructions
│ └── utils.ts # Utility instructions
├── hooks/
│ ├── useMarkets.ts # Fetch all markets
│ ├── useMarket.ts # Fetch single market
│ ├── usePosition.ts # User position data
│ ├── usePositions.ts # All user positions
│ ├── useLiquidations.ts # Liquidation opportunities
│ ├── useFlashLoan.ts # Flash loan helpers
│ └── useAuthorizations.ts # User's delegations
├── math/
│ ├── shares.ts # Share calculations (ERC-4626)
│ ├── interest.ts # APY calculations
│ ├── liquidation.ts # LIF, seized collateral
│ └── health.ts # Health factor logic
├── constants/
│ ├── addresses.ts # Program IDs
│ ├── markets.ts # Known markets
│ └── tokens.ts # Token metadata
└── utils/
├── format.ts # Number formatting
├── transaction.ts # TX builders
└── websocket.ts # Account subscriptions
stores/
├── walletStore.ts # Wallet state
├── marketStore.ts # Market data cache
└── uiStore.ts # UI preferences
Complete Instruction Reference
Admin Instructions (9 total)
| Instruction |
Accounts |
Parameters |
UI Location |
initialize |
payer, protocol_state, system_program |
owner, fee_recipient |
One-time setup |
transfer_ownership |
owner, protocol_state |
new_owner |
Admin → Protocol |
accept_ownership |
pending_owner, protocol_state |
- |
Admin → Protocol |
set_fee_recipient |
owner, protocol_state |
new_recipient |
Admin → Protocol |
set_protocol_paused |
owner, protocol_state |
paused |
Admin → Protocol |
set_market_paused |
owner, protocol_state, market |
market_id, paused |
Admin → Markets |
enable_lltv |
owner, protocol_state |
lltv |
Admin → Whitelist |
enable_irm |
owner, protocol_state |
irm |
Admin → Whitelist |
set_fee |
owner, protocol_state, market |
market_id, fee |
Admin → Markets |
Market Instructions (1 total)
| Instruction |
Accounts |
Parameters |
UI Location |
create_market |
creator, protocol_state, market, collateral_mint, loan_mint, collateral_vault, loan_vault, oracle, irm, token_program, system_program |
collateral_mint, loan_mint, oracle, irm, lltv |
Markets → Create |
Position Instructions (2 total)
| Instruction |
Accounts |
Parameters |
UI Location |
create_position |
payer, owner, market, position, system_program |
market_id |
Auto-prepend |
close_position |
owner, rent_receiver, position |
market_id |
Dashboard |
Supply Instructions (2 total)
| Instruction |
Accounts |
Parameters |
UI Location |
supply |
supplier, protocol_state, market, position, on_behalf_of, supplier_token_account, loan_vault, loan_mint, token_program |
market_id, assets, min_shares |
Market → Supply |
withdraw |
caller, protocol_state, market, position, authorization (optional), receiver_token_account, loan_vault, loan_mint, token_program |
market_id, assets, shares |
Market → Withdraw |
Borrow Instructions (4 total)
| Instruction |
Accounts |
Parameters |
UI Location |
supply_collateral |
depositor, protocol_state, market, position, on_behalf_of, depositor_token_account, collateral_vault, collateral_mint, token_program |
market_id, amount |
Market → Collateral |
withdraw_collateral |
caller, protocol_state, market, position, authorization (optional), oracle, receiver_token_account, collateral_vault, collateral_mint, token_program |
market_id, amount |
Market → Collateral |
borrow |
caller, protocol_state, market, position, authorization (optional), oracle, receiver_token_account, loan_vault, loan_mint, token_program |
market_id, assets, max_shares |
Market → Borrow |
repay |
repayer, market, position, on_behalf_of, repayer_token_account, loan_vault, loan_mint, token_program |
market_id, assets, shares |
Market → Repay |
Liquidation Instructions (1 total)
| Instruction |
Accounts |
Parameters |
UI Location |
liquidate |
liquidator, market, borrower_position, borrower, oracle, liquidator_loan_account, liquidator_collateral_account, loan_vault, collateral_vault, loan_mint, collateral_mint, token_program |
market_id, seized_assets |
Market → Liquidate |
Flash Loan Instructions (3 total)
| Instruction |
Accounts |
Parameters |
UI Location |
flash_loan_start |
borrower, protocol_state, market, borrower_token_account, loan_vault, loan_mint, token_program |
market_id, amount |
Flash Loans |
flash_loan_end |
borrower, market, borrower_token_account, loan_vault, loan_mint, token_program |
market_id, borrowed_amount |
Flash Loans |
flash_loan |
borrower, protocol_state, market, borrower_token_account, loan_vault, loan_mint, token_program |
market_id, amount |
Flash Loans |
Utility Instructions (4 total)
| Instruction |
Accounts |
Parameters |
UI Location |
accrue_interest_ix |
market |
market_id |
Auto-called |
set_authorization |
authorizer, authorized, authorization, system_program |
is_authorized, expires_at |
Settings → Auth |
revoke_authorization |
authorizer, authorization |
- |
Settings → Auth |
claim_fees |
protocol_state, market, fee_position |
market_id |
Admin → Markets |
Implementation Guide
Step 1: Setup Anchor Client
// lib/anchor/client.ts
import { Program, AnchorProvider, Idl } from '@coral-xyz/anchor';
import { Connection, PublicKey } from '@solana/web3.js';
import { AnchorWallet } from '@solana/wallet-adapter-react';
import IDL from './idl.json';
export const MORPHO_PROGRAM_ID = new PublicKey('YOUR_PROGRAM_ID');
export function getMorphoProgram(
connection: Connection,
wallet: AnchorWallet
) {
const provider = new AnchorProvider(connection, wallet, {
commitment: 'confirmed',
});
return new Program(IDL as Idl, MORPHO_PROGRAM_ID, provider);
}
// Derive PDAs
export const PROGRAM_SEED = Buffer.from('morpho');
export function getProtocolStatePDA() {
return PublicKey.findProgramAddressSync(
[PROGRAM_SEED, Buffer.from('protocol_state')],
MORPHO_PROGRAM_ID
);
}
export function getMarketPDA(
collateralMint: PublicKey,
loanMint: PublicKey,
oracle: PublicKey,
irm: PublicKey,
lltv: number
) {
const marketId = calculateMarketId(collateralMint, loanMint, oracle, irm, lltv);
return PublicKey.findProgramAddressSync(
[PROGRAM_SEED, Buffer.from('market'), marketId],
MORPHO_PROGRAM_ID
);
}
export function getPositionPDA(
marketId: Buffer,
owner: PublicKey
) {
return PublicKey.findProgramAddressSync(
[PROGRAM_SEED, Buffer.from('position'), marketId, owner.toBuffer()],
MORPHO_PROGRAM_ID
);
}
export function getAuthorizationPDA(
authorizer: PublicKey,
authorized: PublicKey
) {
return PublicKey.findProgramAddressSync(
[
PROGRAM_SEED,
Buffer.from('authorization'),
authorizer.toBuffer(),
authorized.toBuffer()
],
MORPHO_PROGRAM_ID
);
}
// Market ID calculation (keccak256)
import { keccak256 } from 'js-sha3';
export function calculateMarketId(
collateralMint: PublicKey,
loanMint: PublicKey,
oracle: PublicKey,
irm: PublicKey,
lltv: number
): Buffer {
const data = Buffer.concat([
collateralMint.toBuffer(),
loanMint.toBuffer(),
oracle.toBuffer(),
irm.toBuffer(),
Buffer.from(new BN(lltv).toArray('le', 8))
]);
return Buffer.from(keccak256(data), 'hex');
}
Step 2: Core Instruction Builders
// lib/anchor/instructions/supply.ts
import { BN } from '@coral-xyz/anchor';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { getAssociatedTokenAddress } from '@solana/spl-token';
export async function buildSupplyInstruction(
program: Program,
marketId: Buffer,
supplier: PublicKey,
onBehalfOf: PublicKey,
assets: BN,
minShares: BN
): Promise<TransactionInstruction> {
const [protocolState] = getProtocolStatePDA();
const [market] = getMarketPDA(/* derive from marketId */);
const [position] = getPositionPDA(marketId, onBehalfOf);
const marketAccount = await program.account.market.fetch(market);
const supplierTokenAccount = await getAssociatedTokenAddress(
marketAccount.loanMint,
supplier
);
const [loanVault] = PublicKey.findProgramAddressSync(
[PROGRAM_SEED, Buffer.from('loan_vault'), marketId],
MORPHO_PROGRAM_ID
);
return program.methods
.supply(Array.from(marketId), assets, minShares)
.accounts({
supplier,
protocolState,
market,
position,
onBehalfOf,
supplierTokenAccount,
loanVault,
loanMint: marketAccount.loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
export async function buildWithdrawInstruction(
program: Program,
marketId: Buffer,
caller: PublicKey,
positionOwner: PublicKey,
receiver: PublicKey,
assets: BN,
shares: BN,
authorization?: PublicKey
): Promise<TransactionInstruction> {
// Similar structure to supply
// Key difference: assets OR shares (not both)
// Include authorization account if caller != positionOwner
return program.methods
.withdraw(Array.from(marketId), assets, shares)
.accounts({
caller,
protocolState,
market,
position,
authorization: authorization || null,
receiverTokenAccount,
loanVault,
loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
// lib/anchor/instructions/borrow.ts
export async function buildBorrowInstruction(
program: Program,
marketId: Buffer,
caller: PublicKey,
positionOwner: PublicKey,
receiver: PublicKey,
assets: BN,
maxShares: BN,
oracle: PublicKey,
authorization?: PublicKey
): Promise<TransactionInstruction> {
const [market] = getMarketPDA(/* ... */);
const [position] = getPositionPDA(marketId, positionOwner);
const marketAccount = await program.account.market.fetch(market);
return program.methods
.borrow(Array.from(marketId), assets, maxShares)
.accounts({
caller,
protocolState,
market,
position,
authorization: authorization || null,
oracle,
receiverTokenAccount,
loanVault,
loanMint: marketAccount.loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
export async function buildRepayInstruction(
program: Program,
marketId: Buffer,
repayer: PublicKey,
onBehalfOf: PublicKey,
assets: BN,
shares: BN
): Promise<TransactionInstruction> {
// Similar to withdraw: assets OR shares
return program.methods
.repay(Array.from(marketId), assets, shares)
.accounts({
repayer,
market,
position,
onBehalfOf,
repayerTokenAccount,
loanVault,
loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
// lib/anchor/instructions/flashLoan.ts
export async function buildFlashLoanInstruction(
program: Program,
marketId: Buffer,
borrower: PublicKey,
amount: BN
): Promise<TransactionInstruction> {
// Single-instruction flash loan
// Repayment validated automatically via vault reload
return program.methods
.flashLoan(Array.from(marketId), amount)
.accounts({
borrower,
protocolState,
market,
borrowerTokenAccount,
loanVault,
loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
export async function buildFlashLoanStartInstruction(
program: Program,
marketId: Buffer,
borrower: PublicKey,
amount: BN
): Promise<TransactionInstruction> {
// Two-step: start (locks market, transfers out)
return program.methods
.flashLoanStart(Array.from(marketId), amount)
.accounts({
borrower,
protocolState,
market,
borrowerTokenAccount,
loanVault,
loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
export async function buildFlashLoanEndInstruction(
program: Program,
marketId: Buffer,
borrower: PublicKey,
borrowedAmount: BN
): Promise<TransactionInstruction> {
// Two-step: end (validates repayment, unlocks market)
return program.methods
.flashLoanEnd(Array.from(marketId), borrowedAmount)
.accounts({
borrower,
market,
borrowerTokenAccount,
loanVault,
loanMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
}
Step 3: Position Auto-Creation
// lib/utils/transaction.ts
export async function ensurePositionExists(
program: Program,
marketId: Buffer,
owner: PublicKey
): Promise<TransactionInstruction | null> {
const [position] = getPositionPDA(marketId, owner);
try {
await program.account.position.fetch(position);
return null; // Position exists
} catch (e) {
// Position doesn't exist, create it
return program.methods
.createPosition(Array.from(marketId))
.accounts({
payer: owner,
owner,
market: getMarketPDA(/* ... */)[0],
position,
systemProgram: SystemProgram.programId,
})
.instruction();
}
}
// Usage in any instruction that needs position
export async function buildSupplyTransactionWithPosition(
program: Program,
marketId: Buffer,
supplier: PublicKey,
onBehalfOf: PublicKey,
assets: BN,
minShares: BN
): Promise<TransactionInstruction[]> {
const instructions: TransactionInstruction[] = [];
// Prepend create_position if needed
const createPosIx = await ensurePositionExists(program, marketId, onBehalfOf);
if (createPosIx) {
instructions.push(createPosIx);
}
// Add supply instruction
const supplyIx = await buildSupplyInstruction(
program,
marketId,
supplier,
onBehalfOf,
assets,
minShares
);
instructions.push(supplyIx);
return instructions;
}
Step 4: React Hooks for Data Fetching
// lib/hooks/useMarkets.ts
import { useQuery } from '@tanstack/react-query';
import { useConnection } from '@solana/wallet-adapter-react';
export function useMarkets() {
const { connection } = useConnection();
return useQuery({
queryKey: ['markets'],
queryFn: async () => {
const program = getMorphoProgram(connection, /* ... */);
// Fetch all Market accounts
const markets = await program.account.market.all();
// Enrich with APY calculations, TVL, etc.
return Promise.all(
markets.map(async (m) => {
const supplyAPY = await calculateSupplyAPY(m.account);
const borrowAPY = await calculateBorrowAPY(m.account);
const tvl = calculateTVL(m.account);
return {
publicKey: m.publicKey,
account: m.account,
supplyAPY,
borrowAPY,
tvl,
};
})
);
},
refetchInterval: 10_000, // Refresh every 10s
});
}
// lib/hooks/usePosition.ts
export function usePosition(marketId: Buffer, owner?: PublicKey) {
const { connection } = useConnection();
const { publicKey } = useWallet();
const positionOwner = owner || publicKey;
return useQuery({
queryKey: ['position', marketId.toString('hex'), positionOwner?.toString()],
queryFn: async () => {
if (!positionOwner) return null;
const program = getMorphoProgram(connection, /* ... */);
const [position] = getPositionPDA(marketId, positionOwner);
try {
const account = await program.account.position.fetch(position);
// Fetch parent market for calculations
const [market] = getMarketPDA(/* ... */);
const marketAccount = await program.account.market.fetch(market);
// Calculate current values
const supplyAssets = toAssetsDown(
account.supplyShares,
marketAccount.totalSupplyAssets,
marketAccount.totalSupplyShares
);
const borrowAssets = toAssetsUp(
account.borrowShares,
marketAccount.totalBorrowAssets,
marketAccount.totalBorrowShares
);
// Calculate health factor
const healthFactor = calculateHealthFactor(
account.collateral,
borrowAssets,
marketAccount.lltv,
oraclePrice // fetch from oracle
);
return {
publicKey: position,
account,
supplyAssets,
borrowAssets,
healthFactor,
};
} catch (e) {
return null; // Position doesn't exist
}
},
enabled: !!positionOwner,
refetchInterval: 5_000,
});
}
Step 5: WebSocket Subscriptions
// lib/utils/websocket.ts
import { Connection, PublicKey } from '@solana/web3.js';
export function subscribeToPosition(
connection: Connection,
positionPubkey: PublicKey,
callback: (accountInfo: any) => void
): number {
return connection.onAccountChange(
positionPubkey,
(accountInfo) => {
callback(accountInfo);
},
'confirmed'
);
}
// Usage in React component
export function usePositionSubscription(positionPubkey?: PublicKey) {
const { connection } = useConnection();
const queryClient = useQueryClient();
useEffect(() => {
if (!positionPubkey) return;
const subscriptionId = subscribeToPosition(
connection,
positionPubkey,
() => {
// Invalidate query to refetch
queryClient.invalidateQueries(['position', positionPubkey.toString()]);
}
);
return () => {
connection.removeAccountChangeListener(subscriptionId);
};
}, [positionPubkey, connection, queryClient]);
}
Step 6: Math Libraries (Client-side)
// lib/math/shares.ts
import BN from 'bn.js';
const VIRTUAL_SHARES = new BN(1_000_000); // 1e6
const VIRTUAL_ASSETS = new BN(1);
export function toSharesDown(
assets: BN,
totalAssets: BN,
totalShares: BN
): BN {
// shares = assets * (totalShares + VIRTUAL_SHARES) / (totalAssets + VIRTUAL_ASSETS)
const numerator = assets.mul(totalShares.add(VIRTUAL_SHARES));
const denominator = totalAssets.add(VIRTUAL_ASSETS);
return numerator.div(denominator);
}
export function toSharesUp(
assets: BN,
totalAssets: BN,
totalShares: BN
): BN {
const numerator = assets.mul(totalShares.add(VIRTUAL_SHARES));
const denominator = totalAssets.add(VIRTUAL_ASSETS);
// Ceiling division: (a + b - 1) / b
return numerator.add(denominator).sub(new BN(1)).div(denominator);
}
export function toAssetsDown(
shares: BN,
totalAssets: BN,
totalShares: BN
): BN {
// assets = shares * (totalAssets + VIRTUAL_ASSETS) / (totalShares + VIRTUAL_SHARES)
const numerator = shares.mul(totalAssets.add(VIRTUAL_ASSETS));
const denominator = totalShares.add(VIRTUAL_SHARES);
return numerator.div(denominator);
}
export function toAssetsUp(
shares: BN,
totalAssets: BN,
totalShares: BN
): BN {
const numerator = shares.mul(totalAssets.add(VIRTUAL_ASSETS));
const denominator = totalShares.add(VIRTUAL_SHARES);
return numerator.add(denominator).sub(new BN(1)).div(denominator);
}
// lib/math/health.ts
export function calculateHealthFactor(
collateral: BN,
borrowAssets: BN,
lltv: number, // basis points
oraclePrice: BN // scaled 1e36
): number {
if (borrowAssets.isZero()) return Infinity;
// maxBorrow = collateral * price * lltv / 1e36 / 10000
const maxBorrow = collateral
.mul(oraclePrice)
.mul(new BN(lltv))
.div(new BN(10).pow(new BN(36)))
.div(new BN(10000));
// healthFactor = maxBorrow / borrowAssets
return maxBorrow.mul(new BN(1000)).div(borrowAssets).toNumber() / 1000;
}
export function isLiquidatable(
collateral: BN,
borrowShares: BN,
totalBorrowAssets: BN,
totalBorrowShares: BN,
oraclePrice: BN,
lltv: number
): boolean {
const borrowAssets = toAssetsUp(borrowShares, totalBorrowAssets, totalBorrowShares);
const healthFactor = calculateHealthFactor(collateral, borrowAssets, lltv, oraclePrice);
return healthFactor < 1.0;
}
// lib/math/liquidation.ts
export function calculateLIF(lltv: number): number {
// LIF = min(1.15, 1 / (1 - 0.3 * (1 - lltv/10000)))
const lltvDecimal = lltv / 10000;
const baseLIF = 1 / (1 - 0.3 * (1 - lltvDecimal));
return Math.min(1.15, baseLIF);
}
export function calculateSeizedCollateral(
seizedAssets: BN,
oraclePrice: BN, // 1e36
lif: number
): BN {
// seizedCollateral = seizedAssets * LIF * 1e36 / price
const lifScaled = new BN(Math.floor(lif * 1000)); // Scale LIF by 1000
return seizedAssets
.mul(lifScaled)
.mul(new BN(10).pow(new BN(36)))
.div(oraclePrice)
.div(new BN(1000));
}
Component Implementation Examples
Supply Tab
// components/market/tabs/SupplyTab.tsx
'use client';
import { useState } from 'react';
import { BN } from '@coral-xyz/anchor';
import { useWallet } from '@solana/wallet-adapter-react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { buildSupplyTransactionWithPosition } from '@/lib/anchor/instructions/supply';
import { toSharesDown } from '@/lib/math/shares';
export function SupplyTab({ marketId, market }: Props) {
const { publicKey, sendTransaction } = useWallet();
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const handleSupply = async () => {
if (!publicKey || !amount) return;
setLoading(true);
try {
const assets = new BN(parseFloat(amount) * 10 ** market.loanDecimals);
// Calculate min shares with 1% slippage
const expectedShares = toSharesDown(
assets,
market.totalSupplyAssets,
market.totalSupplyShares
);
const minShares = expectedShares.mul(new BN(99)).div(new BN(100));
// Build transaction with auto position creation
const instructions = await buildSupplyTransactionWithPosition(
program,
marketId,
publicKey,
publicKey, // on_behalf_of
assets,
minShares
);
const tx = new Transaction().add(...instructions);
const signature = await sendTransaction(tx, connection);
await connection.confirmTransaction(signature, 'confirmed');
toast.success('Supply successful!');
} catch (error) {
console.error(error);
toast.error('Supply failed');
} finally {
setLoading(false);
}
};
return (
<div className="space-y-4">
<div>
<label>Amount to Supply</label>
<Input
type="number"
value={amount}
=> setAmount(e.target.value)}
placeholder="0.00"
/>
<div className="text-sm text-muted-foreground mt-1">
Balance: {userBalance} {market.loanSymbol}
</div>
</div>
<div className="border rounded-lg p-4 space-y-2">
<div className="flex justify-between">
<span>You'll receive</span>
<span className="font-mono">
{expectedShares.toString()} shares
</span>
</div>
<div className="flex justify-between">
<span>Supply APY</span>
<span className="text-green-600">{supplyAPY}%</span>
</div>
</div>
<Button
disabled={loading || !amount}
className="w-full"
>
{loading ? 'Supplying...' : 'Supply'}
</Button>
</div>
);
}
Health Factor Component
// components/position/HealthFactor.tsx
export function HealthFactorBar({ healthFactor }: { healthFactor: number }) {
const getColor = (hf: number) => {
if (hf > 1.5) return 'bg-green-500';
if (hf > 1.2) return 'bg-yellow-500';
if (hf > 1.05) return 'bg-orange-500';
return 'bg-red-500';
};
const getLabel = (hf: number) => {
if (hf > 1.5) return 'Safe';
if (hf > 1.2) return 'Caution';
if (hf > 1.05) return 'Warning';
return 'Critical';
};
const percentage = Math.min((healthFactor / 2) * 100, 100);
return (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">Health Factor</span>
<span className="font-mono text-lg">{healthFactor.toFixed(2)}</span>
</div>
<div className="relative h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`absolute h-full ${getColor(healthFactor)} transition-all`}
style={{ width: `${percentage}%` }}
/>
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{getLabel(healthFactor)}</span>
<span>Liquidation at <1.0</span>
</div>
</div>
);
}
Authorization Tab
// components/market/tabs/AuthorizationTab.tsx
export function AuthorizationTab() {
const { publicKey } = useWallet();
const [authorized, setAuthorized] = useState('');
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
const [neverExpires, setNeverExpires] = useState(false);
const { data: authorizations } = useAuthorizations(publicKey);
const handleGrant = async () => {
if (!publicKey || !authorized) return;
const expiryTimestamp = neverExpires
? Number.MAX_SAFE_INTEGER
: Math.floor((expiresAt?.getTime() || Date.now()) / 1000);
const ix = await buildSetAuthorizationInstruction(
program,
publicKey,
new PublicKey(authorized),
true,
new BN(expiryTimestamp)
);
// Send transaction...
};
const handleRevoke = async (authPubkey: PublicKey) => {
const ix = await buildRevokeAuthorizationInstruction(
program,
publicKey,
authPubkey
);
// Send transaction...
};
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Grant New Authorization</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label>Authorized Address</label>
<Input
value={authorized}
=> setAuthorized(e.target.value)}
placeholder="Enter Solana address"
/>
</div>
<div>
<label>Expiry Date</label>
<div className="space-y-2">
<Checkbox
checked={neverExpires}
=> setNeverExpires(checked as boolean)}
>
Never expires
</Checkbox>
{!neverExpires && (
<DatePicker
value={expiresAt}
minDate={new Date()}
/>
)}
</div>
</div>
<Button className="w-full">
Grant Authorization
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Active Authorizations</CardTitle>
</CardHeader>
<CardContent>
{authorizations?.map((auth) => (
<div key={auth.publicKey.toString()} className="border-b py-4">
<div className="flex justify-between items-start">
<div>
<p className="font-mono text-sm">
{auth.account.authorized.toString()}
</p>
<p className="text-xs text-muted-foreground">
{auth.account.isRevoked ? (
<span className="text-red-500">Revoked ×</span>
) : (
<span className="text-green-500">Active ✓</span>
)}
</p>
<p className="text-xs text-muted-foreground">
Expires: {formatDate(auth.account.expiresAt)}
</p>
</div>
{!auth.account.isRevoked && (
<Button
variant="destructive"
size="sm"
=> handleRevoke(auth.publicKey)}
>
Revoke
</Button>
)}
</div>
</div>
))}
</CardContent>
</Card>
</div>
);
}
Critical Implementation Notes
1. Assets vs Shares Flexibility
// withdraw() and repay() accept EITHER assets OR shares
// UI should provide toggle:
function WithdrawTab() {
const [mode, setMode] = useState<'assets' | 'shares'>('assets');
const [amount, setAmount] = useState('');
const handleWithdraw = async () => {
const assets = mode === 'assets' ? new BN(amount) : new BN(0);
const shares = mode === 'shares' ? new BN(amount) : new BN(0);
const ix = await buildWithdrawInstruction(
program,
marketId,
publicKey,
publicKey,
publicKey,
assets,
shares
);
// Send...
};
return (
<div>
<Tabs value={mode}
<TabsList>
<TabsTrigger value="assets">By Amount</TabsTrigger>
<TabsTrigger value="shares">By Shares</TabsTrigger>
</TabsList>
</Tabs>
<Input
value={amount}
=> setAmount(e.target.value)}
placeholder={mode === 'assets' ? 'Amount' : 'Shares'}
/>
</div>
);
}
2. Flash Loan Lock Warning
// When building flash loan transactions, warn user:
function FlashLoanTab() {
return (
<Alert variant="warning">
<AlertTitle>Flash Loan Active</AlertTitle>
<AlertDescription>
During a flash loan, the market is LOCKED (flash_loan_lock = 1).
No other operations can occur until the loan is repaid and lock released.
Single-instruction mode: Repayment is automatic via vault reload.
Two-step mode: You MUST call flash_loan_end() to unlock.
</AlertDescription>
</Alert>
);
}
3. Authorization Validation
// Before allowing delegated operations, check authorization:
async function checkAuthorization(
program: Program,
caller: PublicKey,
owner: PublicKey
): Promise<boolean> {
if (caller.equals(owner)) return true;
const [authPDA] = getAuthorizationPDA(owner, caller);
try {
const auth = await program.account.authorization.fetch(authPDA);
const now = Math.floor(Date.now() / 1000);
return (
auth.isAuthorized &&
!auth.isRevoked &&
auth.expiresAt > now
);
} catch {
return false;
}
}
// Usage in withdraw:
function WithdrawTab({ positionOwner }) {
const { publicKey } = useWallet();
const canWithdraw = await checkAuthorization(program, publicKey, positionOwner);
if (!canWithdraw) {
return <div>You don't have permission to withdraw from this position</div>;
}
// Render withdraw UI...
}
4. Admin Access Control
CRITICAL: Admin routes must verify wallet = protocol owner
// lib/hooks/useProtocolOwner.ts
import { useQuery } from '@tanstack/react-query';
import { useConnection } from '@solana/wallet-adapter-react';
import { PublicKey } from '@solana/web3.js';
export function useProtocolOwner() {
const { connection } = useConnection();
return useQuery({
queryKey: ['protocol-owner'],
queryFn: async () => {
const program = getMorphoProgram(connection);
const [protocolState] = getProtocolStatePDA();
const state = await program.account.protocolState.fetch(protocolState);
return state.owner as PublicKey;
},
staleTime: 60_000, // Cache for 1 minute
});
}
// lib/hooks/useIsAdmin.ts
import { useMemo } from 'react';
import { useWallet } from '@solana/wallet-adapter-react';
import { useProtocolOwner } from './useProtocolOwner';
export function useIsAdmin(): boolean | undefined {
const { publicKey } = useWallet();
const { data: owner, isLoading } = useProtocolOwner();
return useMemo(() => {
if (isLoading) return undefined; // Loading state
if (!publicKey || !owner) return false;
return publicKey.equals(owner);
}, [publicKey, owner, isLoading]);
}
// app/(admin)/admin/layout.tsx - Admin Route Wrapper
'use client';
import { useWallet } from '@solana/wallet-adapter-react';
import { useIsAdmin } from '@/lib/hooks/useIsAdmin';
import { useProtocolOwner } from '@/lib/hooks/useProtocolOwner';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { AlertTriangle, Lock } from 'lucide-react';
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const { publicKey } = useWallet();
const isAdmin = useIsAdmin();
const { data: owner } = useProtocolOwner();
const router = useRouter();
useEffect(() => {
if (publicKey && isAdmin === false) {
// Not admin, redirect to home
router.push('/');
}
}, [publicKey, isAdmin, router]);
// No wallet connected
if (!publicKey) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center max-w-md">
<Lock className="w-16 h-16 mx-auto mb-4 text-gray-400" />
<h2 className="text-2xl font-bold mb-2">Admin Access Required</h2>
<p className="text-gray-600 mb-4">
Please connect your wallet to access the admin panel.
</p>
</div>
</div>
);
}
// Loading owner check
if (isAdmin === undefined) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Verifying admin access...</p>
</div>
</div>
);
}
// Not an admin
if (!isAdmin) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center max-w-md">
<AlertTriangle className="w-16 h-16 mx-auto mb-4 text-red-500" />
<h2 className="text-2xl font-bold mb-2 text-red-600">Access Denied</h2>
<p className="text-gray-600 mb-4">
Only the protocol owner can access the admin panel.
</p>
<div className="bg-gray-100 rounded-lg p-4 mb-4">
<p className="text-xs text-gray-500 mb-1">Protocol Owner:</p>
<p className="text-sm font-mono break-all">{owner?.toString()}</p>
</div>
<p className="text-xs text-gray-500">
Your wallet: {publicKey.toString()}
</p>
</div>
</div>
);
}
// Admin verified - render admin pages
return (
<
…(truncated)
1---2name: morpho-solana-frontend3description: Build production-ready frontend for Morpho Blue lending protocol on Solana. Covers all 26 program instructions across supply/borrow, flash loans, liquidations, authorization, and admin features. Uses Next.js 14, Anchor client, Jupiter wallet adapter, and Kamino-style UI/UX. Integrates with morpho-solana-builder skill for contract understanding.4---5
6# Morpho Solana Frontend Builder
7
8Build a complete, production-ready frontend for the Morpho Blue lending protocol on Solana with all 26 instructions implemented.
9
10## Overview
11
12This skill guides you through building a DeFi lending interface that covers:
13- **Core Lending**: Supply, withdraw, borrow, repay, collateral management
14- **Flash Loans**: All 3 flash loan modes (single-instruction, two-step start/end)
15- **Liquidations**: Unhealthy position scanner and liquidation interface
16- **Authorization**: Time-based delegation system with revocation
17- **Admin Panel**: Protocol management, whitelisting, fee controls
18- **Position Management**: Auto-create positions, close empty positions
19
20## Tech Stack
21
22```typescript
23Core Framework:
24├── Next.js 14 (App Router)
25├── TypeScript
26├── TailwindCSS + shadcn/ui
27└── Framer Motion
28
29Solana Integration:
30├── @solana/web3.js
31├── @solana/wallet-adapter-react (Jupiter)
32├── @coral-xyz/anchor
33└── @solana/spl-token
34
35State Management:
36├── Zustand (global state)
37├── TanStack Query (async state)
38└── WebSocket subscriptions
39
40UI Components:
41├── Recharts (analytics)
42├── Radix UI (primitives)
43└── shadcn/ui (pre-built components)
44```
45
46## Project Structure
47
48```
49app/
50├── (user)/ # User routes (no auth required)
51│ ├── markets/
52│ │ ├── page.tsx # Markets explorer
53│ │ └── [marketId]/
54│ │ └── page.tsx # Market detail
55│ ├── dashboard/page.tsx # User positions
56│ ├── liquidations/page.tsx # Liquidation opportunities
57│ ├── flash-loans/page.tsx # Flash loan interface
58│ ├── settings/page.tsx # User settings
59│ └── page.tsx # Landing page
60├── (admin)/ # Admin routes (owner only)
61│ └── admin/
62│ ├── layout.tsx # Admin auth wrapper
63│ ├── page.tsx # Admin dashboard
64│ ├── protocol/page.tsx # Protocol settings
65│ ├── markets/page.tsx # Market management
66│ └── whitelist/page.tsx # LLTV/IRM whitelist
67└── layout.tsx # Root layout
68
69components/
70├── wallet/
71│ ├── WalletProvider.tsx # Jupiter wallet setup
72│ ├── WalletButton.tsx # Connect button
73│ └── WalletDropdown.tsx # Balance, disconnect
74├── market/
75│ ├── MarketCard.tsx # Market list item
76│ ├── MarketStats.tsx # TVL, APY display
77│ └── tabs/
78│ ├── SupplyTab.tsx # supply()
79│ ├── WithdrawTab.tsx # withdraw()
80│ ├── CollateralTab.tsx # supply_collateral() + withdraw_collateral()
81│ ├── BorrowTab.tsx # borrow()
82│ ├── RepayTab.tsx # repay()
83│ ├── LiquidateTab.tsx # liquidate()
84│ ├── FlashLoanTab.tsx # flash_loan() variants
85│ └── AuthorizationTab.tsx # set_authorization()
86├── position/
87│ ├── PositionCard.tsx # Position display
88│ ├── HealthFactor.tsx # Health visualization
89│ └── PositionManager.tsx # create_position(), close_position()
90├── admin/
91│ ├── ProtocolControls.tsx # Pause, ownership
92│ ├── WhitelistManager.tsx # enable_lltv(), enable_irm()
93│ ├── MarketControls.tsx # set_fee(), set_market_paused()
94│ └── FeeClaimButton.tsx # claim_fees()
95├── shared/
96│ ├── TokenSelector.tsx # Token dropdown
97│ ├── TransactionModal.tsx # TX signing flow
98│ ├── HealthFactorBar.tsx # Visual health
99│ └── APYBadge.tsx # Supply/borrow APY
100└── layout/
101 ├── Header.tsx # Nav + wallet
102 ├── Sidebar.tsx # Navigation
103 └── Footer.tsx
104
105lib/
106├── anchor/
107│ ├── idl.ts # Morpho program IDL
108│ ├── client.ts # Anchor Program instance
109│ └── instructions/
110│ ├── supply.ts # Supply instruction builders
111│ ├── borrow.ts # Borrow instruction builders
112│ ├── liquidate.ts # Liquidation instruction
113│ ├── flashLoan.ts # Flash loan instructions
114│ ├── admin.ts # Admin instructions
115│ └── utils.ts # Utility instructions
116├── hooks/
117│ ├── useMarkets.ts # Fetch all markets
118│ ├── useMarket.ts # Fetch single market
119│ ├── usePosition.ts # User position data
120│ ├── usePositions.ts # All user positions
121│ ├── useLiquidations.ts # Liquidation opportunities
122│ ├── useFlashLoan.ts # Flash loan helpers
123│ └── useAuthorizations.ts # User's delegations
124├── math/
125│ ├── shares.ts # Share calculations (ERC-4626)
126│ ├── interest.ts # APY calculations
127│ ├── liquidation.ts # LIF, seized collateral
128│ └── health.ts # Health factor logic
129├── constants/
130│ ├── addresses.ts # Program IDs
131│ ├── markets.ts # Known markets
132│ └── tokens.ts # Token metadata
133└── utils/
134 ├── format.ts # Number formatting
135 ├── transaction.ts # TX builders
136 └── websocket.ts # Account subscriptions
137
138stores/
139├── walletStore.ts # Wallet state
140├── marketStore.ts # Market data cache
141└── uiStore.ts # UI preferences
142```
143
144## Complete Instruction Reference
145
146### Admin Instructions (9 total)
147
148| Instruction | Accounts | Parameters | UI Location |
149|------------|----------|------------|-------------|
150| `initialize` | payer, protocol_state, system_program | owner, fee_recipient | One-time setup |
151| `transfer_ownership` | owner, protocol_state | new_owner | Admin → Protocol |
152| `accept_ownership` | pending_owner, protocol_state | - | Admin → Protocol |
153| `set_fee_recipient` | owner, protocol_state | new_recipient | Admin → Protocol |
154| `set_protocol_paused` | owner, protocol_state | paused | Admin → Protocol |
155| `set_market_paused` | owner, protocol_state, market | market_id, paused | Admin → Markets |
156| `enable_lltv` | owner, protocol_state | lltv | Admin → Whitelist |
157| `enable_irm` | owner, protocol_state | irm | Admin → Whitelist |
158| `set_fee` | owner, protocol_state, market | market_id, fee | Admin → Markets |
159
160### Market Instructions (1 total)
161
162| Instruction | Accounts | Parameters | UI Location |
163|------------|----------|------------|-------------|
164| `create_market` | creator, protocol_state, market, collateral_mint, loan_mint, collateral_vault, loan_vault, oracle, irm, token_program, system_program | collateral_mint, loan_mint, oracle, irm, lltv | Markets → Create |
165
166### Position Instructions (2 total)
167
168| Instruction | Accounts | Parameters | UI Location |
169|------------|----------|------------|-------------|
170| `create_position` | payer, owner, market, position, system_program | market_id | Auto-prepend |
171| `close_position` | owner, rent_receiver, position | market_id | Dashboard |
172
173### Supply Instructions (2 total)
174
175| Instruction | Accounts | Parameters | UI Location |
176|------------|----------|------------|-------------|
177| `supply` | supplier, protocol_state, market, position, on_behalf_of, supplier_token_account, loan_vault, loan_mint, token_program | market_id, assets, min_shares | Market → Supply |
178| `withdraw` | caller, protocol_state, market, position, authorization (optional), receiver_token_account, loan_vault, loan_mint, token_program | market_id, assets, shares | Market → Withdraw |
179
180### Borrow Instructions (4 total)
181
182| Instruction | Accounts | Parameters | UI Location |
183|------------|----------|------------|-------------|
184| `supply_collateral` | depositor, protocol_state, market, position, on_behalf_of, depositor_token_account, collateral_vault, collateral_mint, token_program | market_id, amount | Market → Collateral |
185| `withdraw_collateral` | caller, protocol_state, market, position, authorization (optional), oracle, receiver_token_account, collateral_vault, collateral_mint, token_program | market_id, amount | Market → Collateral |
186| `borrow` | caller, protocol_state, market, position, authorization (optional), oracle, receiver_token_account, loan_vault, loan_mint, token_program | market_id, assets, max_shares | Market → Borrow |
187| `repay` | repayer, market, position, on_behalf_of, repayer_token_account, loan_vault, loan_mint, token_program | market_id, assets, shares | Market → Repay |
188
189### Liquidation Instructions (1 total)
190
191| Instruction | Accounts | Parameters | UI Location |
192|------------|----------|------------|-------------|
193| `liquidate` | liquidator, market, borrower_position, borrower, oracle, liquidator_loan_account, liquidator_collateral_account, loan_vault, collateral_vault, loan_mint, collateral_mint, token_program | market_id, seized_assets | Market → Liquidate |
194
195### Flash Loan Instructions (3 total)
196
197| Instruction | Accounts | Parameters | UI Location |
198|------------|----------|------------|-------------|
199| `flash_loan_start` | borrower, protocol_state, market, borrower_token_account, loan_vault, loan_mint, token_program | market_id, amount | Flash Loans |
200| `flash_loan_end` | borrower, market, borrower_token_account, loan_vault, loan_mint, token_program | market_id, borrowed_amount | Flash Loans |
201| `flash_loan` | borrower, protocol_state, market, borrower_token_account, loan_vault, loan_mint, token_program | market_id, amount | Flash Loans |
202
203### Utility Instructions (4 total)
204
205| Instruction | Accounts | Parameters | UI Location |
206|------------|----------|------------|-------------|
207| `accrue_interest_ix` | market | market_id | Auto-called |
208| `set_authorization` | authorizer, authorized, authorization, system_program | is_authorized, expires_at | Settings → Auth |
209| `revoke_authorization` | authorizer, authorization | - | Settings → Auth |
210| `claim_fees` | protocol_state, market, fee_position | market_id | Admin → Markets |
211
212## Implementation Guide
213
214### Step 1: Setup Anchor Client
215
216```typescript
217// lib/anchor/client.ts
218import { Program, AnchorProvider, Idl } from '@coral-xyz/anchor';
219import { Connection, PublicKey } from '@solana/web3.js';
220import { AnchorWallet } from '@solana/wallet-adapter-react';
221import IDL from './idl.json';
222
223export const MORPHO_PROGRAM_ID = new PublicKey('YOUR_PROGRAM_ID');
224
225export function getMorphoProgram(
226 connection: Connection,
227 wallet: AnchorWallet
228) {
229 const provider = new AnchorProvider(connection, wallet, {
230 commitment: 'confirmed',
231 });
232
233 return new Program(IDL as Idl, MORPHO_PROGRAM_ID, provider);
234}
235
236// Derive PDAs
237export const PROGRAM_SEED = Buffer.from('morpho');
238
239export function getProtocolStatePDA() {
240 return PublicKey.findProgramAddressSync(
241 [PROGRAM_SEED, Buffer.from('protocol_state')],
242 MORPHO_PROGRAM_ID
243 );
244}
245
246export function getMarketPDA(
247 collateralMint: PublicKey,
248 loanMint: PublicKey,
249 oracle: PublicKey,
250 irm: PublicKey,
251 lltv: number
252) {
253 const marketId = calculateMarketId(collateralMint, loanMint, oracle, irm, lltv);
254 return PublicKey.findProgramAddressSync(
255 [PROGRAM_SEED, Buffer.from('market'), marketId],
256 MORPHO_PROGRAM_ID
257 );
258}
259
260export function getPositionPDA(
261 marketId: Buffer,
262 owner: PublicKey
263) {
264 return PublicKey.findProgramAddressSync(
265 [PROGRAM_SEED, Buffer.from('position'), marketId, owner.toBuffer()],
266 MORPHO_PROGRAM_ID
267 );
268}
269
270export function getAuthorizationPDA(
271 authorizer: PublicKey,
272 authorized: PublicKey
273) {
274 return PublicKey.findProgramAddressSync(
275 [
276 PROGRAM_SEED,
277 Buffer.from('authorization'),
278 authorizer.toBuffer(),
279 authorized.toBuffer()
280 ],
281 MORPHO_PROGRAM_ID
282 );
283}
284
285// Market ID calculation (keccak256)
286import { keccak256 } from 'js-sha3';
287
288export function calculateMarketId(
289 collateralMint: PublicKey,
290 loanMint: PublicKey,
291 oracle: PublicKey,
292 irm: PublicKey,
293 lltv: number
294): Buffer {
295 const data = Buffer.concat([
296 collateralMint.toBuffer(),
297 loanMint.toBuffer(),
298 oracle.toBuffer(),
299 irm.toBuffer(),
300 Buffer.from(new BN(lltv).toArray('le', 8))
301 ]);
302
303 return Buffer.from(keccak256(data), 'hex');
304}
305```
306
307### Step 2: Core Instruction Builders
308
309```typescript
310// lib/anchor/instructions/supply.ts
311import { BN } from '@coral-xyz/anchor';
312import { PublicKey, TransactionInstruction } from '@solana/web3.js';
313import { getAssociatedTokenAddress } from '@solana/spl-token';
314
315export async function buildSupplyInstruction(
316 program: Program,
317 marketId: Buffer,
318 supplier: PublicKey,
319 onBehalfOf: PublicKey,
320 assets: BN,
321 minShares: BN
322): Promise<TransactionInstruction> {
323 const [protocolState] = getProtocolStatePDA();
324 const [market] = getMarketPDA(/* derive from marketId */);
325 const [position] = getPositionPDA(marketId, onBehalfOf);
326
327 const marketAccount = await program.account.market.fetch(market);
328
329 const supplierTokenAccount = await getAssociatedTokenAddress(
330 marketAccount.loanMint,
331 supplier
332 );
333
334 const [loanVault] = PublicKey.findProgramAddressSync(
335 [PROGRAM_SEED, Buffer.from('loan_vault'), marketId],
336 MORPHO_PROGRAM_ID
337 );
338
339 return program.methods
340 .supply(Array.from(marketId), assets, minShares)
341 .accounts({
342 supplier,
343 protocolState,
344 market,
345 position,
346 onBehalfOf,
347 supplierTokenAccount,
348 loanVault,
349 loanMint: marketAccount.loanMint,
350 tokenProgram: TOKEN_PROGRAM_ID,
351 })
352 .instruction();
353}
354
355export async function buildWithdrawInstruction(
356 program: Program,
357 marketId: Buffer,
358 caller: PublicKey,
359 positionOwner: PublicKey,
360 receiver: PublicKey,
361 assets: BN,
362 shares: BN,
363 authorization?: PublicKey
364): Promise<TransactionInstruction> {
365 // Similar structure to supply
366 // Key difference: assets OR shares (not both)
367 // Include authorization account if caller != positionOwner
368
369 return program.methods
370 .withdraw(Array.from(marketId), assets, shares)
371 .accounts({
372 caller,
373 protocolState,
374 market,
375 position,
376 authorization: authorization || null,
377 receiverTokenAccount,
378 loanVault,
379 loanMint,
380 tokenProgram: TOKEN_PROGRAM_ID,
381 })
382 .instruction();
383}
384```
385
386```typescript
387// lib/anchor/instructions/borrow.ts
388export async function buildBorrowInstruction(
389 program: Program,
390 marketId: Buffer,
391 caller: PublicKey,
392 positionOwner: PublicKey,
393 receiver: PublicKey,
394 assets: BN,
395 maxShares: BN,
396 oracle: PublicKey,
397 authorization?: PublicKey
398): Promise<TransactionInstruction> {
399 const [market] = getMarketPDA(/* ... */);
400 const [position] = getPositionPDA(marketId, positionOwner);
401
402 const marketAccount = await program.account.market.fetch(market);
403
404 return program.methods
405 .borrow(Array.from(marketId), assets, maxShares)
406 .accounts({
407 caller,
408 protocolState,
409 market,
410 position,
411 authorization: authorization || null,
412 oracle,
413 receiverTokenAccount,
414 loanVault,
415 loanMint: marketAccount.loanMint,
416 tokenProgram: TOKEN_PROGRAM_ID,
417 })
418 .instruction();
419}
420
421export async function buildRepayInstruction(
422 program: Program,
423 marketId: Buffer,
424 repayer: PublicKey,
425 onBehalfOf: PublicKey,
426 assets: BN,
427 shares: BN
428): Promise<TransactionInstruction> {
429 // Similar to withdraw: assets OR shares
430
431 return program.methods
432 .repay(Array.from(marketId), assets, shares)
433 .accounts({
434 repayer,
435 market,
436 position,
437 onBehalfOf,
438 repayerTokenAccount,
439 loanVault,
440 loanMint,
441 tokenProgram: TOKEN_PROGRAM_ID,
442 })
443 .instruction();
444}
445```
446
447```typescript
448// lib/anchor/instructions/flashLoan.ts
449export async function buildFlashLoanInstruction(
450 program: Program,
451 marketId: Buffer,
452 borrower: PublicKey,
453 amount: BN
454): Promise<TransactionInstruction> {
455 // Single-instruction flash loan
456 // Repayment validated automatically via vault reload
457
458 return program.methods
459 .flashLoan(Array.from(marketId), amount)
460 .accounts({
461 borrower,
462 protocolState,
463 market,
464 borrowerTokenAccount,
465 loanVault,
466 loanMint,
467 tokenProgram: TOKEN_PROGRAM_ID,
468 })
469 .instruction();
470}
471
472export async function buildFlashLoanStartInstruction(
473 program: Program,
474 marketId: Buffer,
475 borrower: PublicKey,
476 amount: BN
477): Promise<TransactionInstruction> {
478 // Two-step: start (locks market, transfers out)
479
480 return program.methods
481 .flashLoanStart(Array.from(marketId), amount)
482 .accounts({
483 borrower,
484 protocolState,
485 market,
486 borrowerTokenAccount,
487 loanVault,
488 loanMint,
489 tokenProgram: TOKEN_PROGRAM_ID,
490 })
491 .instruction();
492}
493
494export async function buildFlashLoanEndInstruction(
495 program: Program,
496 marketId: Buffer,
497 borrower: PublicKey,
498 borrowedAmount: BN
499): Promise<TransactionInstruction> {
500 // Two-step: end (validates repayment, unlocks market)
501
502 return program.methods
503 .flashLoanEnd(Array.from(marketId), borrowedAmount)
504 .accounts({
505 borrower,
506 market,
507 borrowerTokenAccount,
508 loanVault,
509 loanMint,
510 tokenProgram: TOKEN_PROGRAM_ID,
511 })
512 .instruction();
513}
514```
515
516### Step 3: Position Auto-Creation
517
518```typescript
519// lib/utils/transaction.ts
520export async function ensurePositionExists(
521 program: Program,
522 marketId: Buffer,
523 owner: PublicKey
524): Promise<TransactionInstruction | null> {
525 const [position] = getPositionPDA(marketId, owner);
526
527 try {
528 await program.account.position.fetch(position);
529 return null; // Position exists
530 } catch (e) {
531 // Position doesn't exist, create it
532 return program.methods
533 .createPosition(Array.from(marketId))
534 .accounts({
535 payer: owner,
536 owner,
537 market: getMarketPDA(/* ... */)[0],
538 position,
539 systemProgram: SystemProgram.programId,
540 })
541 .instruction();
542 }
543}
544
545// Usage in any instruction that needs position
546export async function buildSupplyTransactionWithPosition(
547 program: Program,
548 marketId: Buffer,
549 supplier: PublicKey,
550 onBehalfOf: PublicKey,
551 assets: BN,
552 minShares: BN
553): Promise<TransactionInstruction[]> {
554 const instructions: TransactionInstruction[] = [];
555
556 // Prepend create_position if needed
557 const createPosIx = await ensurePositionExists(program, marketId, onBehalfOf);
558 if (createPosIx) {
559 instructions.push(createPosIx);
560 }
561
562 // Add supply instruction
563 const supplyIx = await buildSupplyInstruction(
564 program,
565 marketId,
566 supplier,
567 onBehalfOf,
568 assets,
569 minShares
570 );
571 instructions.push(supplyIx);
572
573 return instructions;
574}
575```
576
577### Step 4: React Hooks for Data Fetching
578
579```typescript
580// lib/hooks/useMarkets.ts
581import { useQuery } from '@tanstack/react-query';
582import { useConnection } from '@solana/wallet-adapter-react';
583
584export function useMarkets() {
585 const { connection } = useConnection();
586
587 return useQuery({
588 queryKey: ['markets'],
589 queryFn: async () => {
590 const program = getMorphoProgram(connection, /* ... */);
591
592 // Fetch all Market accounts
593 const markets = await program.account.market.all();
594
595 // Enrich with APY calculations, TVL, etc.
596 return Promise.all(
597 markets.map(async (m) => {
598 const supplyAPY = await calculateSupplyAPY(m.account);
599 const borrowAPY = await calculateBorrowAPY(m.account);
600 const tvl = calculateTVL(m.account);
601
602 return {
603 publicKey: m.publicKey,
604 account: m.account,
605 supplyAPY,
606 borrowAPY,
607 tvl,
608 };
609 })
610 );
611 },
612 refetchInterval: 10_000, // Refresh every 10s
613 });
614}
615
616// lib/hooks/usePosition.ts
617export function usePosition(marketId: Buffer, owner?: PublicKey) {
618 const { connection } = useConnection();
619 const { publicKey } = useWallet();
620
621 const positionOwner = owner || publicKey;
622
623 return useQuery({
624 queryKey: ['position', marketId.toString('hex'), positionOwner?.toString()],
625 queryFn: async () => {
626 if (!positionOwner) return null;
627
628 const program = getMorphoProgram(connection, /* ... */);
629 const [position] = getPositionPDA(marketId, positionOwner);
630
631 try {
632 const account = await program.account.position.fetch(position);
633
634 // Fetch parent market for calculations
635 const [market] = getMarketPDA(/* ... */);
636 const marketAccount = await program.account.market.fetch(market);
637
638 // Calculate current values
639 const supplyAssets = toAssetsDown(
640 account.supplyShares,
641 marketAccount.totalSupplyAssets,
642 marketAccount.totalSupplyShares
643 );
644
645 const borrowAssets = toAssetsUp(
646 account.borrowShares,
647 marketAccount.totalBorrowAssets,
648 marketAccount.totalBorrowShares
649 );
650
651 // Calculate health factor
652 const healthFactor = calculateHealthFactor(
653 account.collateral,
654 borrowAssets,
655 marketAccount.lltv,
656 oraclePrice // fetch from oracle
657 );
658
659 return {
660 publicKey: position,
661 account,
662 supplyAssets,
663 borrowAssets,
664 healthFactor,
665 };
666 } catch (e) {
667 return null; // Position doesn't exist
668 }
669 },
670 enabled: !!positionOwner,
671 refetchInterval: 5_000,
672 });
673}
674```
675
676### Step 5: WebSocket Subscriptions
677
678```typescript
679// lib/utils/websocket.ts
680import { Connection, PublicKey } from '@solana/web3.js';
681
682export function subscribeToPosition(
683 connection: Connection,
684 positionPubkey: PublicKey,
685 callback: (accountInfo: any) => void
686): number {
687 return connection.onAccountChange(
688 positionPubkey,
689 (accountInfo) => {
690 callback(accountInfo);
691 },
692 'confirmed'
693 );
694}
695
696// Usage in React component
697export function usePositionSubscription(positionPubkey?: PublicKey) {
698 const { connection } = useConnection();
699 const queryClient = useQueryClient();
700
701 useEffect(() => {
702 if (!positionPubkey) return;
703
704 const subscriptionId = subscribeToPosition(
705 connection,
706 positionPubkey,
707 () => {
708 // Invalidate query to refetch
709 queryClient.invalidateQueries(['position', positionPubkey.toString()]);
710 }
711 );
712
713 return () => {
714 connection.removeAccountChangeListener(subscriptionId);
715 };
716 }, [positionPubkey, connection, queryClient]);
717}
718```
719
720### Step 6: Math Libraries (Client-side)
721
722```typescript
723// lib/math/shares.ts
724import BN from 'bn.js';
725
726const VIRTUAL_SHARES = new BN(1_000_000); // 1e6
727const VIRTUAL_ASSETS = new BN(1);
728
729export function toSharesDown(
730 assets: BN,
731 totalAssets: BN,
732 totalShares: BN
733): BN {
734 // shares = assets * (totalShares + VIRTUAL_SHARES) / (totalAssets + VIRTUAL_ASSETS)
735 const numerator = assets.mul(totalShares.add(VIRTUAL_SHARES));
736 const denominator = totalAssets.add(VIRTUAL_ASSETS);
737 return numerator.div(denominator);
738}
739
740export function toSharesUp(
741 assets: BN,
742 totalAssets: BN,
743 totalShares: BN
744): BN {
745 const numerator = assets.mul(totalShares.add(VIRTUAL_SHARES));
746 const denominator = totalAssets.add(VIRTUAL_ASSETS);
747
748 // Ceiling division: (a + b - 1) / b
749 return numerator.add(denominator).sub(new BN(1)).div(denominator);
750}
751
752export function toAssetsDown(
753 shares: BN,
754 totalAssets: BN,
755 totalShares: BN
756): BN {
757 // assets = shares * (totalAssets + VIRTUAL_ASSETS) / (totalShares + VIRTUAL_SHARES)
758 const numerator = shares.mul(totalAssets.add(VIRTUAL_ASSETS));
759 const denominator = totalShares.add(VIRTUAL_SHARES);
760 return numerator.div(denominator);
761}
762
763export function toAssetsUp(
764 shares: BN,
765 totalAssets: BN,
766 totalShares: BN
767): BN {
768 const numerator = shares.mul(totalAssets.add(VIRTUAL_ASSETS));
769 const denominator = totalShares.add(VIRTUAL_SHARES);
770
771 return numerator.add(denominator).sub(new BN(1)).div(denominator);
772}
773
774// lib/math/health.ts
775export function calculateHealthFactor(
776 collateral: BN,
777 borrowAssets: BN,
778 lltv: number, // basis points
779 oraclePrice: BN // scaled 1e36
780): number {
781 if (borrowAssets.isZero()) return Infinity;
782
783 // maxBorrow = collateral * price * lltv / 1e36 / 10000
784 const maxBorrow = collateral
785 .mul(oraclePrice)
786 .mul(new BN(lltv))
787 .div(new BN(10).pow(new BN(36)))
788 .div(new BN(10000));
789
790 // healthFactor = maxBorrow / borrowAssets
791 return maxBorrow.mul(new BN(1000)).div(borrowAssets).toNumber() / 1000;
792}
793
794export function isLiquidatable(
795 collateral: BN,
796 borrowShares: BN,
797 totalBorrowAssets: BN,
798 totalBorrowShares: BN,
799 oraclePrice: BN,
800 lltv: number
801): boolean {
802 const borrowAssets = toAssetsUp(borrowShares, totalBorrowAssets, totalBorrowShares);
803 const healthFactor = calculateHealthFactor(collateral, borrowAssets, lltv, oraclePrice);
804 return healthFactor < 1.0;
805}
806
807// lib/math/liquidation.ts
808export function calculateLIF(lltv: number): number {
809 // LIF = min(1.15, 1 / (1 - 0.3 * (1 - lltv/10000)))
810 const lltvDecimal = lltv / 10000;
811 const baseLIF = 1 / (1 - 0.3 * (1 - lltvDecimal));
812 return Math.min(1.15, baseLIF);
813}
814
815export function calculateSeizedCollateral(
816 seizedAssets: BN,
817 oraclePrice: BN, // 1e36
818 lif: number
819): BN {
820 // seizedCollateral = seizedAssets * LIF * 1e36 / price
821 const lifScaled = new BN(Math.floor(lif * 1000)); // Scale LIF by 1000
822
823 return seizedAssets
824 .mul(lifScaled)
825 .mul(new BN(10).pow(new BN(36)))
826 .div(oraclePrice)
827 .div(new BN(1000));
828}
829```
830
831## Component Implementation Examples
832
833### Supply Tab
834
835```typescript
836// components/market/tabs/SupplyTab.tsx
837'use client';
838
839import { useState } from 'react';
840import { BN } from '@coral-xyz/anchor';
841import { useWallet } from '@solana/wallet-adapter-react';
842import { Input } from '@/components/ui/input';
843import { Button } from '@/components/ui/button';
844import { buildSupplyTransactionWithPosition } from '@/lib/anchor/instructions/supply';
845import { toSharesDown } from '@/lib/math/shares';
846
847export function SupplyTab({ marketId, market }: Props) {
848 const { publicKey, sendTransaction } = useWallet();
849 const [amount, setAmount] = useState('');
850 const [loading, setLoading] = useState(false);
851
852 const handleSupply = async () => {
853 if (!publicKey || !amount) return;
854
855 setLoading(true);
856 try {
857 const assets = new BN(parseFloat(amount) * 10 ** market.loanDecimals);
858
859 // Calculate min shares with 1% slippage
860 const expectedShares = toSharesDown(
861 assets,
862 market.totalSupplyAssets,
863 market.totalSupplyShares
864 );
865 const minShares = expectedShares.mul(new BN(99)).div(new BN(100));
866
867 // Build transaction with auto position creation
868 const instructions = await buildSupplyTransactionWithPosition(
869 program,
870 marketId,
871 publicKey,
872 publicKey, // on_behalf_of
873 assets,
874 minShares
875 );
876
877 const tx = new Transaction().add(...instructions);
878 const signature = await sendTransaction(tx, connection);
879
880 await connection.confirmTransaction(signature, 'confirmed');
881
882 toast.success('Supply successful!');
883 } catch (error) {
884 console.error(error);
885 toast.error('Supply failed');
886 } finally {
887 setLoading(false);
888 }
889 };
890
891 return (
892 <div className="space-y-4">
893 <div>
894 <label>Amount to Supply</label>
895 <Input
896 type="number"
897 value={amount}
898 onChange={(e) => setAmount(e.target.value)}
899 placeholder="0.00"
900 />
901 <div className="text-sm text-muted-foreground mt-1">
902 Balance: {userBalance} {market.loanSymbol}
903 </div>
904 </div>
905
906 <div className="border rounded-lg p-4 space-y-2">
907 <div className="flex justify-between">
908 <span>You'll receive</span>
909 <span className="font-mono">
910 {expectedShares.toString()} shares
911 </span>
912 </div>
913 <div className="flex justify-between">
914 <span>Supply APY</span>
915 <span className="text-green-600">{supplyAPY}%</span>
916 </div>
917 </div>
918
919 <Button
920 onClick={handleSupply}
921 disabled={loading || !amount}
922 className="w-full"
923 >
924 {loading ? 'Supplying...' : 'Supply'}
925 </Button>
926 </div>
927 );
928}
929```
930
931### Health Factor Component
932
933```typescript
934// components/position/HealthFactor.tsx
935export function HealthFactorBar({ healthFactor }: { healthFactor: number }) {
936 const getColor = (hf: number) => {
937 if (hf > 1.5) return 'bg-green-500';
938 if (hf > 1.2) return 'bg-yellow-500';
939 if (hf > 1.05) return 'bg-orange-500';
940 return 'bg-red-500';
941 };
942
943 const getLabel = (hf: number) => {
944 if (hf > 1.5) return 'Safe';
945 if (hf > 1.2) return 'Caution';
946 if (hf > 1.05) return 'Warning';
947 return 'Critical';
948 };
949
950 const percentage = Math.min((healthFactor / 2) * 100, 100);
951
952 return (
953 <div className="space-y-2">
954 <div className="flex justify-between items-center">
955 <span className="text-sm font-medium">Health Factor</span>
956 <span className="font-mono text-lg">{healthFactor.toFixed(2)}</span>
957 </div>
958
959 <div className="relative h-2 bg-gray-200 rounded-full overflow-hidden">
960 <div
961 className={`absolute h-full ${getColor(healthFactor)} transition-all`}
962 style={{ width: `${percentage}%` }}
963 />
964 </div>
965
966 <div className="flex justify-between text-xs text-muted-foreground">
967 <span>{getLabel(healthFactor)}</span>
968 <span>Liquidation at <1.0</span>
969 </div>
970 </div>
971 );
972}
973```
974
975### Authorization Tab
976
977```typescript
978// components/market/tabs/AuthorizationTab.tsx
979export function AuthorizationTab() {
980 const { publicKey } = useWallet();
981 const [authorized, setAuthorized] = useState('');
982 const [expiresAt, setExpiresAt] = useState<Date | null>(null);
983 const [neverExpires, setNeverExpires] = useState(false);
984
985 const { data: authorizations } = useAuthorizations(publicKey);
986
987 const handleGrant = async () => {
988 if (!publicKey || !authorized) return;
989
990 const expiryTimestamp = neverExpires
991 ? Number.MAX_SAFE_INTEGER
992 : Math.floor((expiresAt?.getTime() || Date.now()) / 1000);
993
994 const ix = await buildSetAuthorizationInstruction(
995 program,
996 publicKey,
997 new PublicKey(authorized),
998 true,
999 new BN(expiryTimestamp)
1000 );
1001
1002 // Send transaction...
1003 };
1004
1005 const handleRevoke = async (authPubkey: PublicKey) => {
1006 const ix = await buildRevokeAuthorizationInstruction(
1007 program,
1008 publicKey,
1009 authPubkey
1010 );
1011
1012 // Send transaction...
1013 };
1014
1015 return (
1016 <div className="space-y-6">
1017 <Card>
1018 <CardHeader>
1019 <CardTitle>Grant New Authorization</CardTitle>
1020 </CardHeader>
1021 <CardContent className="space-y-4">
1022 <div>
1023 <label>Authorized Address</label>
1024 <Input
1025 value={authorized}
1026 onChange={(e) => setAuthorized(e.target.value)}
1027 placeholder="Enter Solana address"
1028 />
1029 </div>
1030
1031 <div>
1032 <label>Expiry Date</label>
1033 <div className="space-y-2">
1034 <Checkbox
1035 checked={neverExpires}
1036 onCheckedChange={(checked) => setNeverExpires(checked as boolean)}
1037 >
1038 Never expires
1039 </Checkbox>
1040 {!neverExpires && (
1041 <DatePicker
1042 value={expiresAt}
1043 onChange={setExpiresAt}
1044 minDate={new Date()}
1045 />
1046 )}
1047 </div>
1048 </div>
1049
1050 <Button onClick={handleGrant} className="w-full">
1051 Grant Authorization
1052 </Button>
1053 </CardContent>
1054 </Card>
1055
1056 <Card>
1057 <CardHeader>
1058 <CardTitle>Active Authorizations</CardTitle>
1059 </CardHeader>
1060 <CardContent>
1061 {authorizations?.map((auth) => (
1062 <div key={auth.publicKey.toString()} className="border-b py-4">
1063 <div className="flex justify-between items-start">
1064 <div>
1065 <p className="font-mono text-sm">
1066 {auth.account.authorized.toString()}
1067 </p>
1068 <p className="text-xs text-muted-foreground">
1069 {auth.account.isRevoked ? (
1070 <span className="text-red-500">Revoked ×</span>
1071 ) : (
1072 <span className="text-green-500">Active ✓</span>
1073 )}
1074 </p>
1075 <p className="text-xs text-muted-foreground">
1076 Expires: {formatDate(auth.account.expiresAt)}
1077 </p>
1078 </div>
1079
1080 {!auth.account.isRevoked && (
1081 <Button
1082 variant="destructive"
1083 size="sm"
1084 onClick={() => handleRevoke(auth.publicKey)}
1085 >
1086 Revoke
1087 </Button>
1088 )}
1089 </div>
1090 </div>
1091 ))}
1092 </CardContent>
1093 </Card>
1094 </div>
1095 );
1096}
1097```
1098
1099## Critical Implementation Notes
1100
1101### 1. Assets vs Shares Flexibility
1102
1103```typescript
1104// withdraw() and repay() accept EITHER assets OR shares
1105// UI should provide toggle:
1106
1107function WithdrawTab() {
1108 const [mode, setMode] = useState<'assets' | 'shares'>('assets');
1109 const [amount, setAmount] = useState('');
1110
1111 const handleWithdraw = async () => {
1112 const assets = mode === 'assets' ? new BN(amount) : new BN(0);
1113 const shares = mode === 'shares' ? new BN(amount) : new BN(0);
1114
1115 const ix = await buildWithdrawInstruction(
1116 program,
1117 marketId,
1118 publicKey,
1119 publicKey,
1120 publicKey,
1121 assets,
1122 shares
1123 );
1124
1125 // Send...
1126 };
1127
1128 return (
1129 <div>
1130 <Tabs value={mode} onValueChange={setMode}>
1131 <TabsList>
1132 <TabsTrigger value="assets">By Amount</TabsTrigger>
1133 <TabsTrigger value="shares">By Shares</TabsTrigger>
1134 </TabsList>
1135 </Tabs>
1136
1137 <Input
1138 value={amount}
1139 onChange={(e) => setAmount(e.target.value)}
1140 placeholder={mode === 'assets' ? 'Amount' : 'Shares'}
1141 />
1142 </div>
1143 );
1144}
1145```
1146
1147### 2. Flash Loan Lock Warning
1148
1149```typescript
1150// When building flash loan transactions, warn user:
1151
1152function FlashLoanTab() {
1153 return (
1154 <Alert variant="warning">
1155 <AlertTitle>Flash Loan Active</AlertTitle>
1156 <AlertDescription>
1157 During a flash loan, the market is LOCKED (flash_loan_lock = 1).
1158 No other operations can occur until the loan is repaid and lock released.
1159
1160 Single-instruction mode: Repayment is automatic via vault reload.
1161 Two-step mode: You MUST call flash_loan_end() to unlock.
1162 </AlertDescription>
1163 </Alert>
1164 );
1165}
1166```
1167
1168### 3. Authorization Validation
1169
1170```typescript
1171// Before allowing delegated operations, check authorization:
1172
1173async function checkAuthorization(
1174 program: Program,
1175 caller: PublicKey,
1176 owner: PublicKey
1177): Promise<boolean> {
1178 if (caller.equals(owner)) return true;
1179
1180 const [authPDA] = getAuthorizationPDA(owner, caller);
1181
1182 try {
1183 const auth = await program.account.authorization.fetch(authPDA);
1184 const now = Math.floor(Date.now() / 1000);
1185
1186 return (
1187 auth.isAuthorized &&
1188 !auth.isRevoked &&
1189 auth.expiresAt > now
1190 );
1191 } catch {
1192 return false;
1193 }
1194}
1195
1196// Usage in withdraw:
1197function WithdrawTab({ positionOwner }) {
1198 const { publicKey } = useWallet();
1199 const canWithdraw = await checkAuthorization(program, publicKey, positionOwner);
1200
1201 if (!canWithdraw) {
1202 return <div>You don't have permission to withdraw from this position</div>;
1203 }
1204
1205 // Render withdraw UI...
1206}
1207```
1208
1209### 4. Admin Access Control
1210
1211**CRITICAL: Admin routes must verify wallet = protocol owner**
1212
1213```typescript
1214// lib/hooks/useProtocolOwner.ts
1215import { useQuery } from '@tanstack/react-query';
1216import { useConnection } from '@solana/wallet-adapter-react';
1217import { PublicKey } from '@solana/web3.js';
1218
1219export function useProtocolOwner() {
1220 const { connection } = useConnection();
1221
1222 return useQuery({
1223 queryKey: ['protocol-owner'],
1224 queryFn: async () => {
1225 const program = getMorphoProgram(connection);
1226 const [protocolState] = getProtocolStatePDA();
1227 const state = await program.account.protocolState.fetch(protocolState);
1228 return state.owner as PublicKey;
1229 },
1230 staleTime: 60_000, // Cache for 1 minute
1231 });
1232}
1233
1234// lib/hooks/useIsAdmin.ts
1235import { useMemo } from 'react';
1236import { useWallet } from '@solana/wallet-adapter-react';
1237import { useProtocolOwner } from './useProtocolOwner';
1238
1239export function useIsAdmin(): boolean | undefined {
1240 const { publicKey } = useWallet();
1241 const { data: owner, isLoading } = useProtocolOwner();
1242
1243 return useMemo(() => {
1244 if (isLoading) return undefined; // Loading state
1245 if (!publicKey || !owner) return false;
1246 return publicKey.equals(owner);
1247 }, [publicKey, owner, isLoading]);
1248}
1249
1250// app/(admin)/admin/layout.tsx - Admin Route Wrapper
1251'use client';
1252
1253import { useWallet } from '@solana/wallet-adapter-react';
1254import { useIsAdmin } from '@/lib/hooks/useIsAdmin';
1255import { useProtocolOwner } from '@/lib/hooks/useProtocolOwner';
1256import { useRouter } from 'next/navigation';
1257import { useEffect } from 'react';
1258import { AlertTriangle, Lock } from 'lucide-react';
1259
1260export default function AdminLayout({ children }: { children: React.ReactNode }) {
1261 const { publicKey } = useWallet();
1262 const isAdmin = useIsAdmin();
1263 const { data: owner } = useProtocolOwner();
1264 const router = useRouter();
1265
1266 useEffect(() => {
1267 if (publicKey && isAdmin === false) {
1268 // Not admin, redirect to home
1269 router.push('/');
1270 }
1271 }, [publicKey, isAdmin, router]);
1272
1273 // No wallet connected
1274 if (!publicKey) {
1275 return (
1276 <div className="flex items-center justify-center min-h-screen bg-gray-50">
1277 <div className="text-center max-w-md">
1278 <Lock className="w-16 h-16 mx-auto mb-4 text-gray-400" />
1279 <h2 className="text-2xl font-bold mb-2">Admin Access Required</h2>
1280 <p className="text-gray-600 mb-4">
1281 Please connect your wallet to access the admin panel.
1282 </p>
1283 </div>
1284 </div>
1285 );
1286 }
1287
1288 // Loading owner check
1289 if (isAdmin === undefined) {
1290 return (
1291 <div className="flex items-center justify-center min-h-screen bg-gray-50">
1292 <div className="text-center">
1293 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
1294 <p className="text-gray-600">Verifying admin access...</p>
1295 </div>
1296 </div>
1297 );
1298 }
1299
1300 // Not an admin
1301 if (!isAdmin) {
1302 return (
1303 <div className="flex items-center justify-center min-h-screen bg-gray-50">
1304 <div className="text-center max-w-md">
1305 <AlertTriangle className="w-16 h-16 mx-auto mb-4 text-red-500" />
1306 <h2 className="text-2xl font-bold mb-2 text-red-600">Access Denied</h2>
1307 <p className="text-gray-600 mb-4">
1308 Only the protocol owner can access the admin panel.
1309 </p>
1310 <div className="bg-gray-100 rounded-lg p-4 mb-4">
1311 <p className="text-xs text-gray-500 mb-1">Protocol Owner:</p>
1312 <p className="text-sm font-mono break-all">{owner?.toString()}</p>
1313 </div>
1314 <p className="text-xs text-gray-500">
1315 Your wallet: {publicKey.toString()}
1316 </p>
1317 </div>
1318 </div>
1319 );
1320 }
1321
1322 // Admin verified - render admin pages
1323 return (
1324 <
1325
1326…(truncated)