Defi Staking Yield Vault
blockfinanceNodeka and Uniswap-inspired decentralized finance staking yield vault, liquidity pool APY calculator, and reward claimer with live TVL metrics, multi-tier lockup multipliers, and real-time yield simulator.
Installation
$pnpm dlx uipkge-ng@latest add defi-staking-yield-vault$npx uipkge-ng@latest add defi-staking-yield-vault$yarn dlx uipkge-ng@latest add defi-staking-yield-vault$bunx uipkge-ng@latest add defi-staking-yield-vaultManifest:
https://uipkge.dev/r/angular/defi-staking-yield-vault.jsonInstalls to:src/app/components/blocks/defi-staking-yield-vault/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
VaultTokenOptioninterface VaultTokenOption {
symbol: string
name: string
price: number
balance: number
apyBoost: number
description: string
iconColor: string
}VaultLockupOptioninterface VaultLockupOption {
id: string
label: string
durationText: string
days: number
boost: number
badge: string
}VaultIteminterface VaultItem {
id: string
name: string
pairSymbol: string
protocol: string
apy: string
apyRaw: number
tvl: string
risk: 'Low Risk' | 'Medium Risk' | 'High Risk'
riskVariant: 'emerald' | 'amber' | 'rose'
strategy: string
metricLabel: string
metricValue: string
isCurrentVault?: boolean
}Files installed (2)
src/app/components/blocks/defi-staking-yield-vault/defi-staking-yield-vault.component.ts35.7 kB
// Nodeka and Uniswap-inspired DeFi staking yield vault: wallet connection header with // audit + TVL badges, four staking performance KPI cards, a deposit & stake simulator // (token selector, amount input with presets + slider, lockup multipliers, live yield // estimates), and a yield vaults directory. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowUpRight, Check, Coins, Copy, Flame, Fuel, Lock, LucideAngularModule, Percent, RefreshCw, ShieldAlert, ShieldCheck, TrendingUp, Unlock, Wallet, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' interface VaultTokenOption { symbol: string name: string price: number balance: number apyBoost: number description: string iconColor: string } const tokenOptions: VaultTokenOption[] = [ { symbol: 'ETH', name: 'Native Ether', price: 3450.0, balance: 32.5, apyBoost: 0, description: 'Base Layer Staking', iconColor: '#627EEA', }, { symbol: 'stETH', name: 'Lido Staked ETH', price: 3450.0, balance: 15.2, apyBoost: 0, description: '1:1 Rebase Token', iconColor: '#00A3FF', }, { symbol: 'wstETH', name: 'Wrapped stETH', price: 4015.0, balance: 8.4, apyBoost: 0.15, description: 'Compounding Value Token', iconColor: '#0052FF', }, ] interface VaultLockupOption { id: string label: string durationText: string days: number boost: number badge: string } const lockupOptions: VaultLockupOption[] = [ { id: 'flex', label: 'Flexible', durationText: '0-Day Lock', days: 0, boost: 0, badge: 'Flexible' }, { id: '30d', label: '30 Days', durationText: '30-Day Lock', days: 30, boost: 0.5, badge: '+0.5% APY' }, { id: '90d', label: '90 Days', durationText: '90-Day Lock', days: 90, boost: 1.2, badge: '+1.2% APY' }, { id: '365d', label: '365 Days', durationText: '365-Day Lock', days: 365, boost: 2.5, badge: '+2.5% APY' }, ] interface VaultItem { id: string name: string pairSymbol: string protocol: string apy: string apyRaw: number tvl: string risk: 'Low Risk' | 'Medium Risk' | 'High Risk' riskVariant: 'emerald' | 'amber' | 'rose' strategy: string metricLabel: string metricValue: string isCurrentVault?: boolean } const yieldVaults: VaultItem[] = [ { id: 'eth-usdc-lp', name: 'ETH-USDC LP', pairSymbol: 'ETH + USDC', protocol: 'Uniswap v3 · Concentrated', apy: '12.40%', apyRaw: 12.4, tvl: '$58,420,000.00', risk: 'Medium Risk', riskVariant: 'amber', strategy: 'Automated Rebalancing Range ±5%', metricLabel: '24h Volume', metricValue: '$4.2M Vol', }, { id: 'liquid-staked-eth', name: 'Liquid Staked ETH', pairSymbol: 'ETH / stETH', protocol: 'Lido & Distributed Validator', apy: '7.42%', apyRaw: 7.42, tvl: '$142,850,000.00', risk: 'Low Risk', riskVariant: 'emerald', strategy: 'Consensus Layer + MEV Boost', metricLabel: 'Lockup Period', metricValue: 'Instant 0-Day', isCurrentVault: true, }, { id: 'stablecoin-vault', name: 'Stablecoin Multi-Vault', pairSymbol: 'USDC + USDT + DAI', protocol: 'Curve & Morpho Blue Optimizer', apy: '9.80%', apyRaw: 9.8, tvl: '$94,180,000.00', risk: 'Low Risk', riskVariant: 'emerald', strategy: 'Multi-Asset Lending & Arbitrage Yield', metricLabel: 'Coverage', metricValue: 'Nexus Mutual Protected', }, ] const walletAddress = '0x71C856402244243b92834b9d09c2534575823a9F' function fmtUsd(v: number): string { return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-defi-staking-yield-vault, [ui-defi-staking-yield-vault]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSeparatorComponent, UiSliderComponent, ], template: ` <div data-slot="defi-staking-yield-vault" [class]="rootClass"> <!-- Header Section --> <header class="border-border/80 bg-card/60 rounded-xl border p-4 shadow-xs backdrop-blur-xs sm:p-6"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-2"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <lucide-icon [img]="Zap" class="size-5" /> </div> <div> <div class="flex flex-wrap items-center gap-2"> <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> ETH Liquid Staking & Yield Vault v3 </h1> <span ui-badge variant="outline" class="border-border/80 font-mono text-xs font-normal"> v3.2 Protocol </span> </div> </div> </div> <div class="flex flex-wrap items-center gap-2.5 text-xs"> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 font-normal"> <lucide-icon [img]="ShieldCheck" class="size-3.5" aria-hidden="true" /> Audited by Trail of Bits & OpenZeppelin </span> <span ui-badge variant="secondary" class="gap-1.5 font-medium tabular-nums"> <span class="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true"></span> $142,850,000.00 TVL </span> <span class="text-muted-foreground hidden items-center gap-1 sm:inline-flex"> <lucide-icon [img]="Zap" class="text-warning size-3" aria-hidden="true" /> Auto-Compounding Enabled </span> </div> </div> <!-- Wallet Connection Action --> <div class="flex flex-wrap items-center gap-2.5"> @if (isWalletConnected()) { <div class="border-border bg-muted/60 flex items-center gap-2 rounded-lg border px-3 py-1.5 shadow-xs"> <span class="bg-success size-2 rounded-full" aria-hidden="true"></span> <span class="text-foreground font-mono text-xs font-medium">{{ shortAddress }}</span> <button ui-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-5" aria-label="Copy wallet address" (click)="copyAddress()" > @if (copied()) { <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" /> } @else { <lucide-icon [img]="Copy" class="size-3" aria-hidden="true" /> } </button> </div> <button ui-button variant="outline" size="sm" class="shadow-xs active:scale-[0.98]" (click)="isWalletConnected.set(false)" > Disconnect </button> } @else { <button ui-button size="sm" class="gap-2 shadow-xs active:scale-[0.98]" (click)="isWalletConnected.set(true)" > <lucide-icon [img]="Wallet" class="size-4" aria-hidden="true" /> Connect Web3 Wallet </button> } </div> </div> </header> <!-- 4 Staking Performance KPI Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Net Staking APY </p> <div class="border-success/30 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border" aria-hidden="true" > <lucide-icon [img]="TrendingUp" class="size-4" /> </div> </div> <div class="mt-1 flex items-baseline gap-2"> <span class="text-success text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">7.42% APY</span> </div> </div> <div ui-card-content class="text-muted-foreground space-y-1 text-xs"> <div class="text-success flex items-center gap-1.5 font-medium"> <span class="bg-success size-1.5 rounded-full" aria-hidden="true"></span> Auto-Compounding daily </div> <p class="text-muted-foreground tabular-nums">Base 4.92% + 2.50% MEV Boost</p> </div> </div> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Your Staked Balance </p> <div class="border-primary/30 bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md border" aria-hidden="true" > <lucide-icon [img]="Coins" class="size-4" /> </div> </div> <div class="mt-1 flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">24.50 ETH</span> </div> </div> <div ui-card-content class="text-muted-foreground space-y-1 text-xs"> <div class="text-muted-foreground font-medium tabular-nums">≈ $84,525.00 USD</div> <div class="flex items-center gap-1"> <span ui-badge variant="outline" class="border-border/80 px-1.5 py-0 text-xs font-normal" >Active in Vault v3</span > </div> </div> </div> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Unclaimed Rewards </p> <div class="border-warning/30 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border" aria-hidden="true" > <lucide-icon [img]="Coins" class="size-4" /> </div> </div> <div class="mt-1 flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl" >{{ unclaimedEth() }} stETH</span > </div> </div> <div ui-card-content class="space-y-2.5 text-xs"> <div class="text-muted-foreground font-medium tabular-nums">≈ \${{ unclaimedUsd() }} USD</div> <button ui-button variant="outline" size="sm" class="h-7 w-full gap-1.5 text-xs font-medium shadow-xs" [disabled]="unclaimedEth() <= 0 || isClaiming()" (click)="handleClaimRewards()" > @if (isClaiming()) { <lucide-icon [img]="RefreshCw" class="size-3 animate-spin" aria-hidden="true" /> } @else if (claimSuccess()) { <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" /> } {{ isClaiming() ? 'Claiming On-Chain...' : claimSuccess() ? 'Rewards Claimed!' : 'Claim Rewards' }} </button> </div> </div> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Staking Pool Share </p> <div class="border-chart-1/30 bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md border" aria-hidden="true" > <lucide-icon [img]="Percent" class="size-4" /> </div> </div> <div class="mt-1 flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">0.017%</span> </div> </div> <div ui-card-content class="text-muted-foreground space-y-2 text-xs"> <div class="flex items-center justify-between"> <span>of total vault</span> <span class="text-foreground font-medium tabular-nums">$142.85M TVL</span> </div> <div class="bg-muted/80 h-1.5 w-full overflow-hidden rounded-full"> <div class="bg-primary h-full w-[17%] rounded-full"></div> </div> </div> </div> </div> <!-- 2-Column Staking Simulator & Yield Directory --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Deposit & Stake Calculator (7 Cols) --> <div class="space-y-6 lg:col-span-7"> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex items-center justify-between"> <div class="space-y-1"> <h3 ui-card-title class="text-foreground flex items-center gap-2 text-base font-semibold sm:text-lg"> <lucide-icon [img]="Zap" class="text-primary size-5" aria-hidden="true" /> Deposit & Stake Calculator </h3> <p ui-card-description class="text-muted-foreground text-xs"> Simulate staking yields with custom lockup multipliers and real-time auto-compounding. </p> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-semibold" > {{ effectiveApy() }}% APY </span> </div> </div> <div ui-card-content class="space-y-5"> <!-- Token Selector Tabs --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <label class="text-foreground font-medium">Select Staking Asset</label> <span class="tabular-nums">Available: {{ currentToken().balance }} {{ currentToken().symbol }}</span> </div> <div class="grid grid-cols-3 gap-2"> @for (token of tokens; track token.symbol) { <button type="button" [class]="tokenButtonClass(token.symbol)" (click)="selectedTokenSymbol.set(token.symbol)" > <div class="flex w-full items-center justify-between"> <div class="flex items-center gap-2"> <span class="size-5 shrink-0 rounded-full shadow-xs" [style.background-color]="token.iconColor" aria-hidden="true" ></span> <span class="text-foreground text-xs font-semibold">{{ token.symbol }}</span> </div> @if (selectedTokenSymbol() === token.symbol) { <lucide-icon [img]="Check" class="text-primary size-3.5" aria-hidden="true" /> } </div> <span class="text-muted-foreground text-xs tabular-nums">{{ token.balance }} avail.</span> </button> } </div> </div> <!-- Amount Input & Quick Percentages --> <div class="bg-muted/40 border-border/80 space-y-3 rounded-lg border p-4"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <label for="stake-amount-input-angular" class="text-foreground font-medium">Deposit Amount</label> <div class="flex items-center gap-1.5"> <span class="tabular-nums">Wallet: {{ currentToken().balance }} {{ currentToken().symbol }}</span> <button type="button" class="text-primary hover:text-primary/80 inline-flex min-h-6 items-center px-1 font-semibold uppercase transition-colors focus-visible:outline-none" (click)="setMaxAmount()" > MAX </button> </div> </div> <div class="flex items-center justify-between gap-3"> <ui-input id="stake-amount-input-angular" [value]="stakeAmountStr()" (valueChange)="stakeAmountStr.set($event)" type="number" step="any" min="0" class="h-9 border-none bg-transparent px-0 text-xl font-bold tracking-tight tabular-nums shadow-none focus-visible:ring-0" placeholder="0.0" /> <div class="border-border bg-background flex shrink-0 items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-semibold shadow-xs" > <span>{{ currentToken().symbol }}</span> </div> </div> <div class="flex flex-wrap items-center justify-between gap-2 pt-1 text-xs"> <span class="text-muted-foreground tabular-nums">≈ \${{ stakedFiatValue() }} USD</span> <div class="flex items-center gap-1"> @for (pct of [25, 50, 75, 100]; track pct) { <button type="button" class="border-border bg-background hover:bg-muted text-muted-foreground hover:text-foreground min-h-6 rounded border px-2 py-0.5 font-mono text-xs transition-[colors,transform] duration-150 focus-visible:ring-1 focus-visible:outline-none active:scale-95" (click)="setPercentageAmount(pct)" > {{ pct === 100 ? 'MAX' : pct + '%' }} </button> } </div> </div> <div class="pt-2"> <ui-slider [value]="[sliderAmount()]" (valueChange)="handleSliderChange($event)" [min]="0" [max]="currentToken().balance" [step]="0.1" class="w-full" /> </div> </div> <!-- Lockup Duration Selector --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Lockup Duration & Boost</span> <span class="text-muted-foreground">Longer lockups multiply daily yields</span> </div> <div class="grid grid-cols-2 gap-2 sm:grid-cols-4"> @for (opt of lockups; track opt.id) { <button type="button" [class]="lockupButtonClass(opt.id)" (click)="selectedLockupId.set(opt.id)"> <div class="flex items-center gap-1"> @if (opt.days === 0) { <lucide-icon [img]="Unlock" class="text-muted-foreground size-3.5" aria-hidden="true" /> } @else { <lucide-icon [img]="Lock" class="text-primary size-3.5" aria-hidden="true" /> } <span class="text-foreground text-xs font-semibold">{{ opt.label }}</span> </div> <span ui-badge variant="outline" [class]="lockupBadgeClass(opt)">{{ opt.badge }}</span> </button> } </div> </div> <!-- Estimated Returns Highlight Box --> <div class="border-success/20 bg-success/5 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-foreground flex items-center gap-1.5 text-xs font-semibold"> <lucide-icon [img]="Flame" class="text-success size-4" aria-hidden="true" /> Estimated Annual Yield </span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold" > {{ effectiveApy() }}% APY </span> </div> <div class="flex flex-wrap items-baseline gap-2"> <span class="text-success text-xl font-bold tracking-tight tabular-nums sm:text-2xl" >+\${{ annualYieldUsd() }} / year</span > <span class="text-muted-foreground font-mono text-xs tabular-nums" >({{ annualYieldEth() }} {{ currentToken().symbol }})</span > </div> <ui-separator class="bg-success/20" /> <div class="grid grid-cols-2 gap-3 text-xs sm:grid-cols-3"> <div class="space-y-0.5"> <div class="text-muted-foreground">Daily Returns</div> <div class="text-foreground font-medium tabular-nums">+\${{ dailyYieldUsd() }}</div> <div class="text-muted-foreground font-mono text-xs tabular-nums"> {{ dailyYieldEth() }} {{ currentToken().symbol }} </div> </div> <div class="space-y-0.5"> <div class="text-muted-foreground">Monthly Returns</div> <div class="text-foreground font-medium tabular-nums">+\${{ monthlyYieldUsd() }}</div> <div class="text-muted-foreground font-mono text-xs tabular-nums"> {{ monthlyYieldEth() }} {{ currentToken().symbol }} </div> </div> <div class="col-span-2 space-y-0.5 sm:col-span-1"> <div class="text-muted-foreground">Network Gas & Fee</div> <div class="text-foreground flex items-center gap-1 font-medium tabular-nums"> <lucide-icon [img]="Fuel" class="text-muted-foreground size-3" aria-hidden="true" /> ~$2.85 (0% fee) </div> <div class="text-success text-xs">No exit penalty</div> </div> </div> </div> <!-- Primary Stake CTA Button --> <button ui-button size="lg" class="w-full gap-2 font-semibold shadow-xs active:scale-[0.98]" [disabled]="parsedAmount() <= 0 || isStaking()" (click)="handleExecuteStake()" > @if (isStaking()) { <lucide-icon [img]="RefreshCw" class="size-4 animate-spin" aria-hidden="true" /> } @else if (stakeSuccess()) { <lucide-icon [img]="Check" class="text-success size-4" aria-hidden="true" /> } @else { <lucide-icon [img]="Zap" class="size-4" aria-hidden="true" /> } {{ isStaking() ? 'Submitting Staking Transaction...' : stakeSuccess() ? 'Successfully Staked ' + parsedAmount() + ' ' + currentToken().symbol + '!' : 'Stake ' + (parsedAmount() > 0 ? parsedAmount() + ' ' + currentToken().symbol : 'ETH') + ' Now' }} </button> <div class="text-muted-foreground flex items-center justify-center gap-2 text-center text-xs"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" aria-hidden="true" /> <span>Smart contracts verified on Etherscan · 100% non-custodial</span> </div> </div> </div> </div> <!-- Right Column: Yield Vaults Directory (5 Cols) --> <div class="space-y-6 lg:col-span-5"> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <div> <h3 ui-card-title class="text-foreground text-base font-semibold">Yield Vaults Directory</h3> <p ui-card-description class="text-muted-foreground text-xs"> Active liquidity pools and automated yield strategies. </p> </div> <span ui-badge variant="secondary" class="text-xs font-normal tabular-nums">3 Vaults Active</span> </div> </div> <div ui-card-content class="space-y-3.5"> @for (vault of vaults; track vault.id) { <div [class]="vaultCardClass(vault)"> <div class="flex items-start justify-between gap-2"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-semibold">{{ vault.name }}</span> @if (vault.isCurrentVault) { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary px-1.5 py-0 text-xs font-medium" >Current</span > } </div> <p class="text-muted-foreground text-xs">{{ vault.protocol }}</p> </div> <div class="text-right"> <div class="text-success text-base font-bold tabular-nums sm:text-lg">{{ vault.apy }} APY</div> <div class="text-muted-foreground font-mono text-xs tabular-nums">{{ vault.tvl }}</div> </div> </div> <div class="border-border/60 bg-muted/30 flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-xs" > <div class="flex items-center gap-1.5"> <span ui-badge variant="outline" [class]="riskBadgeClass(vault)">{{ vault.risk }}</span> <span class="text-muted-foreground max-w-[140px] truncate sm:max-w-none">{{ vault.strategy }}</span> </div> <div class="text-muted-foreground font-medium tabular-nums">{{ vault.metricValue }}</div> </div> <div class="flex items-center justify-between pt-1"> <span class="text-muted-foreground font-mono text-xs">{{ vault.pairSymbol }}</span> <button ui-button [variant]="vault.isCurrentVault ? 'default' : 'outline'" size="sm" class="h-7 px-3 text-xs shadow-xs" > <span>{{ vault.isCurrentVault ? 'Manage Vault' : 'Deposit Pool' }}</span> <lucide-icon [img]="ArrowUpRight" class="ml-1 size-3" aria-hidden="true" /> </button> </div> </div> } </div> <div ui-card-footer class="border-border/60 bg-muted/20 flex flex-col gap-2 rounded-b-xl border-t p-3.5 text-xs" > <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="ShieldAlert" class="text-warning size-4 shrink-0" aria-hidden="true" /> <span>Yields adjust dynamically with Ethereum validator queue and DEX trading fee volumes.</span> </div> </div> </div> </div> </div> </div> `, }) export class UiDefiStakingYieldVaultComponent { protected readonly ArrowUpRight = ArrowUpRight protected readonly Check = Check protected readonly Coins = Coins protected readonly Copy = Copy protected readonly Flame = Flame protected readonly Fuel = Fuel protected readonly Lock = Lock protected readonly Percent = Percent protected readonly RefreshCw = RefreshCw protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly TrendingUp = TrendingUp protected readonly Unlock = Unlock protected readonly Wallet = Wallet protected readonly Zap = Zap protected readonly tokens = tokenOptions protected readonly lockups = lockupOptions protected readonly vaults = yieldVaults protected readonly shortAddress = '0x71C...3a9F' @Input() set initialToken(v: string) { if (v) this.selectedTokenSymbol.set(v) } @Input() set initialLockup(v: string) { if (v) this.selectedLockupId.set(v) } @Input('class') className?: string readonly isWalletConnected = signal(true) readonly copied = signal(false) readonly unclaimedEth = signal(0.485) readonly isClaiming = signal(false) readonly claimSuccess = signal(false) readonly selectedTokenSymbol = signal('ETH') readonly selectedLockupId = signal('flex') readonly stakeAmountStr = signal('10.0') readonly isStaking = signal(false) readonly stakeSuccess = signal(false) readonly currentToken = computed( () => tokenOptions.find((t) => t.symbol === this.selectedTokenSymbol()) ?? tokenOptions[0]!, ) readonly currentLockup = computed( () => lockupOptions.find((l) => l.id === this.selectedLockupId()) ?? lockupOptions[0]!, ) readonly parsedAmount = computed(() => { const val = parseFloat(this.stakeAmountStr()) return isNaN(val) || val < 0 ? 0 : val }) readonly sliderAmount = computed(() => Math.min(this.parsedAmount(), this.currentToken().balance)) readonly baseVaultApy = 7.42 readonly effectiveApy = computed(() => (this.baseVaultApy + this.currentLockup().boost + this.currentToken().apyBoost).toFixed(2), ) readonly stakedFiatValue = computed(() => fmtUsd(this.parsedAmount() * this.currentToken().price)) readonly unclaimedUsd = computed(() => fmtUsd(this.unclaimedEth() * 3450)) private yieldUsd(divisor: number): string { const rate = parseFloat(this.effectiveApy()) / 100 return fmtUsd((this.parsedAmount() * rate * this.currentToken().price) / divisor) } private yieldEth(divisor: number, digits: number): string { const rate = parseFloat(this.effectiveApy()) / 100 return ((this.parsedAmount() * rate) / divisor).toFixed(digits) } readonly annualYieldUsd = computed(() => this.yieldUsd(1)) readonly annualYieldEth = computed(() => this.yieldEth(1, 3)) readonly monthlyYieldUsd = computed(() => this.yieldUsd(12)) readonly monthlyYieldEth = computed(() => this.yieldEth(12, 4)) readonly dailyYieldUsd = computed(() => this.yieldUsd(365)) readonly dailyYieldEth = computed(() => this.yieldEth(365, 5)) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } tokenButtonClass(symbol: string): string { return cn( 'border-border/80 focus-visible:ring-ring flex flex-col items-start gap-1 rounded-lg border p-3 text-left transition-[colors,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98]', this.selectedTokenSymbol() === symbol ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1' : 'bg-card hover:bg-muted/50 text-muted-foreground', ) } lockupButtonClass(id: string): string { return cn( 'border-border/80 focus-visible:ring-ring flex flex-col items-center justify-center gap-1 rounded-lg border p-2.5 text-center transition-[colors,transform] duration-150 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98]', this.selectedLockupId() === id ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1' : 'bg-card hover:bg-muted/50 text-muted-foreground', ) } lockupBadgeClass(opt: VaultLockupOption): string { return cn( 'border-border/60 font-mono text-xs font-normal tabular-nums', opt.boost > 0 ? 'border-success/30 text-success' : 'text-muted-foreground', ) } vaultCardClass(vault: VaultItem): string { return cn( 'border-border/80 relative space-y-3 rounded-lg border p-3.5 transition-colors', vault.isCurrentVault ? 'bg-primary/5 border-primary/40 ring-primary/20 ring-1' : 'bg-card hover:bg-muted/30', ) } riskBadgeClass(vault: VaultItem): string { return cn( 'px-1.5 py-0 text-xs font-normal', vault.riskVariant === 'emerald' ? 'border-success/30 bg-success/10 text-success' : 'border-warning/30 bg-warning/10 text-warning', ) } copyAddress(): void { try { void navigator.clipboard?.writeText(walletAddress) } catch { // Clipboard unavailable in some contexts; the flag still flips for feedback. } this.copied.set(true) setTimeout(() => this.copied.set(false), 2000) } handleClaimRewards(): void { if (this.unclaimedEth() <= 0 || this.isClaiming()) return this.isClaiming.set(true) setTimeout(() => { this.isClaiming.set(false) this.claimSuccess.set(true) this.unclaimedEth.set(0) setTimeout(() => this.claimSuccess.set(false), 3500) }, 1200) } handleSliderChange(vals: number[]): void { if (vals?.[0] !== undefined) this.stakeAmountStr.set(vals[0].toFixed(2)) } setPercentageAmount(pct: number): void { this.stakeAmountStr.set((this.currentToken().balance * (pct / 100)).toFixed(2)) } setMaxAmount(): void { this.stakeAmountStr.set(this.currentToken().balance.toFixed(2)) } handleExecuteStake(): void { if (this.parsedAmount() <= 0 || this.isStaking()) return this.isStaking.set(true) setTimeout(() => { this.isStaking.set(false) this.stakeSuccess.set(true) setTimeout(() => this.stakeSuccess.set(false), 3500) }, 1400) } }src/app/components/blocks/defi-staking-yield-vault/index.ts0.1 kB
export { UiDefiStakingYieldVaultComponent } from './defi-staking-yield-vault.component'
Raw manifest:https://uipkge.dev/r/angular/defi-staking-yield-vault.json