Menu

Change language

GitHub repo

Defi Staking Yield Vault

blockfinance

Nodeka 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

$npx uipkge-ng@latest add defi-staking-yield-vault
Manifest:https://uipkge.dev/r/angular/defi-staking-yield-vault.jsonInstalls to:src/app/components/blocks/defi-staking-yield-vault/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

VaultTokenOption
interface VaultTokenOption {
  symbol: string
  name: string
  price: number
  balance: number
  apyBoost: number
  description: string
  iconColor: string
}
VaultLockupOption
interface VaultLockupOption {
  id: string
  label: string
  durationText: string
  days: number
  boost: number
  badge: string
}
VaultItem
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
}

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

Raw manifest:https://uipkge.dev/r/angular/defi-staking-yield-vault.json