Crypto Wallet Portfolio
blockfinanceCoinbase and Phantom-style multi-asset crypto holdings dashboard: treasury header with address copy and network badge, portfolio balance hero with 24h PnL and multi-asset allocation bar, asset holdings table with real-time token metrics, and an interactive quick swap calculator.
Installation
$pnpm dlx uipkge-ng@latest add crypto-wallet-portfolio$npx uipkge-ng@latest add crypto-wallet-portfolio$yarn dlx uipkge-ng@latest add crypto-wallet-portfolio$bunx uipkge-ng@latest add crypto-wallet-portfolioManifest:
https://uipkge.dev/r/angular/crypto-wallet-portfolio.jsonInstalls to:src/app/components/blocks/crypto-wallet-portfolio/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.
CryptoAssetinterface CryptoAsset {
id: string
name: string
symbol: string
price: string
rawPrice: number
change24h: string
isPositive: boolean
balance: string
rawBalance: number
fiatValue: string
allocation: string
color: string
}Files installed (2)
src/app/components/blocks/crypto-wallet-portfolio/crypto-wallet-portfolio.component.ts32.7 kB
// Coinbase / Phantom-style multi-asset crypto holdings dashboard: treasury header // with address copy and network badge, portfolio balance hero with 24h PnL and a // multi-asset allocation bar, an asset holdings table, and an interactive quick // swap calculator. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { ArrowDownLeft, ArrowDownUp, ArrowLeftRight, ArrowUpRight, Check, Copy, Fuel, LucideAngularModule, RefreshCw, ShieldCheck, TrendingDown, TrendingUp, Wallet, } 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, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface CryptoAsset { id: string name: string symbol: string price: string rawPrice: number change24h: string isPositive: boolean balance: string rawBalance: number fiatValue: string allocation: string color: string } export const cryptoAssets: CryptoAsset[] = [ { id: 'eth', name: 'Ethereum', symbol: 'ETH', price: '$3,450.00', rawPrice: 3450.0, change24h: '+3.2%', isPositive: true, balance: '24.5 ETH', rawBalance: 24.5, fiatValue: '$84,525.00', allocation: '56.8%', color: '#627EEA', }, { id: 'btc', name: 'Bitcoin', symbol: 'BTC', price: '$64,200.00', rawPrice: 64200.0, change24h: '+1.8%', isPositive: true, balance: '0.58 BTC', rawBalance: 0.58, fiatValue: '$37,236.00', allocation: '25.0%', color: '#F7931A', }, { id: 'sol', name: 'Solana', symbol: 'SOL', price: '$148.50', rawPrice: 148.5, change24h: '-1.1%', isPositive: false, balance: '120.4 SOL', rawBalance: 120.4, fiatValue: '$17,879.40', allocation: '12.0%', color: '#14F195', }, { id: 'usdc', name: 'USD Coin', symbol: 'USDC', price: '$1.00', rawPrice: 1.0, change24h: '+0.01%', isPositive: true, balance: '7,450.00 USDC', rawBalance: 7450.0, fiatValue: '$7,450.00', allocation: '5.0%', color: '#2775CA', }, { id: 'link', name: 'Chainlink', symbol: 'LINK', price: '$18.30', rawPrice: 18.3, change24h: '+5.4%', isPositive: true, balance: '100.0 LINK', rawBalance: 100.0, fiatValue: '$1,830.10', allocation: '1.2%', color: '#375BD2', }, ] export const walletFullAddress = '0x71C856402244243b92834b9d09c2534575823a9F' export const walletShortAddress = '0x71C...3a9F' const slippageOptions = ['0.1', '0.5', '1.0'] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-crypto-wallet-portfolio, [ui-crypto-wallet-portfolio]', 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, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="crypto-wallet-portfolio" [class]="rootClass"> <!-- Header Section --> <header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1.5"> <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]="Wallet" class="size-5" /> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight">Main Treasury Vault</h1> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 font-normal"> <span class="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true"></span> Ethereum Mainnet </span> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs"> <span class="bg-muted/70 text-foreground border-border/80 rounded border px-2 py-0.5 font-mono font-medium"> {{ shortAddress }} </span> <button ui-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-6" aria-label="Copy wallet address" (click)="copyAddress()" > @if (copied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } </button> <span class="inline-flex items-center gap-1"> <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" /> Multi-Sig (3 of 5) </span> </div> </div> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="gap-1.5 shadow-xs"> <lucide-icon [img]="ArrowUpRight" class="size-4" /> Send </button> <button ui-button variant="outline" size="sm" class="gap-1.5 shadow-xs"> <lucide-icon [img]="ArrowDownLeft" class="size-4" /> Receive </button> <button ui-button size="sm" class="gap-1.5 shadow-xs"> <lucide-icon [img]="ArrowLeftRight" class="size-4" /> Swap </button> </div> </header> <!-- Main Grid: Portfolio Overview + Holdings Table & Quick Swap Calculator --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Hero Portfolio Card & Holdings Table (8 cols) --> <div class="space-y-6 lg:col-span-8"> <!-- Hero Portfolio Balance Card --> <div ui-card class="border-border overflow-hidden shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Total Portfolio Value </p> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span class="bg-success size-2 rounded-full" aria-hidden="true"></span> Live Oracle Feed </div> </div> <div class="mt-2 flex flex-wrap items-baseline gap-3"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl"> $148,920.50 </span> <div class="border-success/20 bg-success/10 text-success text-success inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold tabular-nums" > <lucide-icon [img]="TrendingUp" class="size-3.5" /> +$6,420.10 (+4.51%) <span class="text-muted-foreground ml-0.5 font-normal">24h</span> </div> </div> </div> <div ui-card-content class="space-y-4 pt-1"> <!-- Stacked Allocation Bar --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Asset Allocation</span> <span class="tabular-nums">5 Assets</span> </div> <div class="bg-muted/60 border-border/50 flex h-3 w-full gap-0.5 overflow-hidden rounded-full border p-0.5" > @for (asset of assets; track asset.id) { <div [style.width]="asset.allocation" [style.backgroundColor]="asset.color" class="h-full transition-opacity duration-300 first:rounded-l-full last:rounded-r-full hover:opacity-90" [attr.title]="asset.name + ' (' + asset.symbol + '): ' + asset.allocation" ></div> } </div> <!-- Allocation Legend --> <div class="flex flex-wrap items-center gap-x-4 gap-y-2 pt-1 text-xs"> @for (asset of assets; track asset.id) { <div class="flex items-center gap-1.5"> <span class="size-2.5 shrink-0 rounded-full shadow-xs" [style.backgroundColor]="asset.color" aria-hidden="true" ></span> <span class="text-foreground font-medium">{{ asset.symbol }}</span> <span class="text-muted-foreground tabular-nums">{{ asset.allocation }}</span> </div> } </div> </div> </div> </div> <!-- Asset Holdings Table Card --> <div ui-card class="border-border 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">Asset Holdings</h3> <p ui-card-description class="text-muted-foreground text-xs"> Real-time token prices, balances, and portfolio share. </p> </div> <span ui-badge variant="secondary" class="text-xs font-normal tabular-nums"> 5 Assets </span> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="pl-6">Asset</th> <th ui-table-head class="text-right">Price (24h)</th> <th ui-table-head class="text-right">Holdings</th> <th ui-table-head class="hidden text-right sm:table-cell">Portfolio Share</th> <th ui-table-head class="pr-6 text-right">Actions</th> </tr> </thead> <tbody ui-table-body> @for (asset of assets; track asset.id) { <tr ui-table-row class="hover:bg-muted/40 transition-colors"> <!-- Token Icon, Name, Symbol --> <td ui-table-cell class="py-3.5 pl-6"> <div class="flex items-center gap-3"> @if (asset.id === 'eth') { <svg viewBox="0 0 32 32" class="size-8 shrink-0 rounded-full shadow-xs" aria-hidden="true" > <circle cx="16" cy="16" r="16" fill="#627EEA" /> <path fill="#ffffff" fill-opacity="0.6" d="M16.5 4v8.87l7.5 3.35z" /> <path fill="#ffffff" d="M16.5 4L9 16.22l7.5-3.35z" /> <path fill="#ffffff" fill-opacity="0.6" d="M16.5 21.97v6.03L24 17.62z" /> <path fill="#ffffff" d="M16.5 28v-6.03L9 17.62z" /> <path fill="#ffffff" fill-opacity="0.2" d="M16.5 20.57l7.5-4.35-7.5-3.35z" /> <path fill="#ffffff" fill-opacity="0.6" d="M9 16.22l7.5 4.35v-7.7z" /> </svg> } @else if (asset.id === 'btc') { <svg viewBox="0 0 32 32" class="size-8 shrink-0 rounded-full shadow-xs" aria-hidden="true" > <circle cx="16" cy="16" r="16" fill="#F7931A" /> <path fill="#ffffff" d="M22.5 13.5c.3-1.9-1.2-2.9-3.2-3.6l.7-2.6-1.6-.4-.6 2.5c-.4-.1-.9-.2-1.3-.3l.6-2.5-1.6-.4-.7 2.6c-.3-.1-.7-.2-1-.2l-2.2-.5-.4 1.7s.9.2.9.2c.5.1.6.4.6.6l-.6 2.5c0 0 .1 0 .2 0l-.2 0-.9 3.5c-.1.2-.3.4-.6.3 0 0-.9-.2-.9-.2l-.8 1.8 2.1.5c.4.1.8.2 1.2.3l-.7 2.7 1.6.4.7-2.6c.4.1.9.2 1.3.3l-.7 2.7 1.6.4.7-2.7c2.8.5 4.8.3 5.7-2.2.7-2-.1-3.1-1.5-3.8 1.1-.3 1.8-1 2-2.5zm-3.6 5.4c-.5 2-3.9.9-5 .6l.9-3.6c1.1.3 4.6.8 4.1 3zm.5-5.6c-.5 1.8-3.3.9-4.2.7l.8-3.3c.9.2 3.8.7 3.4 2.6z" /> </svg> } @else if (asset.id === 'sol') { <svg viewBox="0 0 32 32" class="size-8 shrink-0 rounded-full shadow-xs" aria-hidden="true" > <circle cx="16" cy="16" r="16" fill="#14151a" /> <path fill="#14F195" d="M8.5 21.8l1.8-1.8c.3-.3.7-.5 1.1-.5h10.4c.5 0 .9.6.6 1l-1.8 1.8c-.3.3-.7.5-1.1.5H9.1c-.5 0-.9-.6-.6-1z" /> <path fill="#9945FF" d="M8.5 12.8l1.8-1.8c.3-.3.7-.5 1.1-.5h10.4c.5 0 .9.6.6 1l-1.8 1.8c-.3.3-.7.5-1.1.5H9.1c-.5 0-.9-.6-.6-1z" /> <path fill="#00C2FF" d="M23.5 17.3l-1.8 1.8c-.3.3-.7.5-1.1.5H10.2c-.5 0-.9-.6-.6-1l1.8-1.8c.3-.3.7-.5 1.1-.5h10.4c.5 0 .9.6.6 1z" /> </svg> } @else if (asset.id === 'usdc') { <svg viewBox="0 0 32 32" class="size-8 shrink-0 rounded-full shadow-xs" aria-hidden="true" > <circle cx="16" cy="16" r="16" fill="#2775CA" /> <path fill="#ffffff" d="M16 6a10 10 0 1010 10A10.01 10.01 0 0016 6zm0 18a8 8 0 118-8 8.01 8.01 0 01-8 8zm1-12h-2v1.1a3.5 3.5 0 00-2 3.1c0 2 1.5 2.7 3 3.1 1.2.3 1.8.6 1.8 1.2s-.6 1.1-1.6 1.1a3.4 3.4 0 01-2.4-.9l-.8 1.4a4.8 4.8 0 003 1.1V20h2v-1.1a3.4 3.4 0 002-3.1c0-2.1-1.6-2.8-3.1-3.2-1.1-.3-1.7-.6-1.7-1.1 0-.5.5-1 1.5-1a3.1 3.1 0 012 .7l.8-1.4a4.5 4.5 0 00-2.5-.9z" /> </svg> } @else { <svg viewBox="0 0 32 32" class="size-8 shrink-0 rounded-full shadow-xs" aria-hidden="true" > <circle cx="16" cy="16" r="16" fill="#375BD2" /> <path fill="#ffffff" d="M16 7l-7.8 4.5v9L16 25l7.8-4.5v-9L16 7zm5.2 12l-5.2 3-5.2-3v-6l5.2-3 5.2 3v6z" /> </svg> } <div> <div class="text-foreground flex items-center gap-1.5 text-sm font-medium"> {{ asset.name }} </div> <div class="text-muted-foreground font-mono text-xs">{{ asset.symbol }}</div> </div> </div> </td> <!-- Current Price & 24h Change --> <td ui-table-cell class="py-3.5 text-right"> <div class="text-foreground text-sm font-medium tabular-nums">{{ asset.price }}</div> <div [class]="trendClass(asset)"> @if (asset.isPositive) { <lucide-icon [img]="TrendingUp" class="size-3" /> } @else { <lucide-icon [img]="TrendingDown" class="size-3" /> } {{ asset.change24h }} </div> </td> <!-- Holdings Amount & Fiat Value --> <td ui-table-cell class="py-3.5 text-right"> <div class="text-foreground text-sm font-medium tabular-nums">{{ asset.balance }}</div> <div class="text-muted-foreground text-xs tabular-nums">{{ asset.fiatValue }}</div> </td> <!-- Portfolio Share --> <td ui-table-cell class="hidden py-3.5 text-right sm:table-cell"> <span ui-badge variant="outline" class="border-border/80 font-mono text-xs tabular-nums"> {{ asset.allocation }} </span> </td> <!-- Actions --> <td ui-table-cell class="py-3.5 pr-6 text-right"> <div class="flex items-center justify-end gap-1.5"> <button ui-button variant="outline" size="sm" class="h-7 px-2.5 text-xs shadow-xs" (click)="selectTradePair(asset.symbol)" > Trade </button> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" > Transfer </button> </div> </td> </tr> } </tbody> </table> </div> </div> </div> </div> <!-- Right Column: Quick Swap Widget & Security Info (4 cols) --> <div class="space-y-6 lg:col-span-4"> <!-- Quick Swap Card --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <h3 ui-card-title class="text-foreground flex items-center gap-1.5 text-base font-semibold"> <lucide-icon [img]="RefreshCw" class="text-primary size-4" /> Quick Swap </h3> <!-- Slippage Settings --> <div class="flex items-center gap-1"> @for (opt of slippageOptions; track opt) { <button type="button" [class]="slippageClass(opt)" (click)="slippage.set(opt)">{{ opt }}%</button> } </div> </div> <p ui-card-description class="text-muted-foreground text-xs"> Swap instant liquidity across decentralized pools. </p> </div> <div ui-card-content class="space-y-3"> <!-- From Token Box --> <div class="bg-muted/40 border-border/80 space-y-2 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <label for="swap-pay-input" class="font-medium">You Pay</label> <div class="flex items-center gap-1.5"> <span class="tabular-nums">Balance: {{ fromAsset().balance }}</span> <button type="button" class="text-primary focus-visible:ring-ring min-h-6 rounded text-xs font-semibold uppercase hover:underline focus-visible:ring-1 focus-visible:outline-none" (click)="setMaxPay()" > MAX </button> </div> </div> <div class="flex items-center justify-between gap-3"> <ui-input id="swap-pay-input" [value]="payAmount()" (valueChange)="payAmount.set($event)" type="number" step="any" min="0" class="h-8 border-none px-0 text-lg font-semibold tabular-nums shadow-none focus-within:ring-0" placeholder="0.0" /> <!-- From Token Selector Pill --> <div class="bg-background border-border flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 shadow-xs select-none" > <span class="size-2 shrink-0 rounded-full" [style.backgroundColor]="fromAsset().color" aria-hidden="true" ></span> <span class="text-foreground text-xs font-semibold">{{ fromAsset().symbol }}</span> </div> </div> <div class="text-muted-foreground text-xs tabular-nums">≈ {{ fiatPayFormatted() }}</div> </div> <!-- Invert Flip Button --> <div class="relative z-10 -my-1 flex justify-center"> <button ui-button variant="outline" size="icon-sm" class="border-border bg-background hover:bg-muted text-muted-foreground hover:text-foreground size-8 rounded-full shadow-xs transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95" aria-label="Flip swap token pair" (click)="flipTokens()" > <lucide-icon [img]="ArrowDownUp" class="size-4" /> </button> </div> <!-- To Token Box --> <div class="bg-muted/40 border-border/80 space-y-2 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="font-medium">You Receive</span> <span class="tabular-nums">Balance: {{ toAsset().balance }}</span> </div> <div class="flex items-center justify-between gap-3"> <div class="text-foreground flex h-8 items-center text-lg font-semibold tabular-nums"> {{ calculatedReceive() }} </div> <!-- To Token Selector Pill --> <div class="bg-background border-border flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 shadow-xs select-none" > <span class="size-2 shrink-0 rounded-full" [style.backgroundColor]="toAsset().color" aria-hidden="true" ></span> <span class="text-foreground text-xs font-semibold">{{ toAsset().symbol }}</span> </div> </div> <div class="text-muted-foreground text-xs tabular-nums">≈ {{ fiatReceiveFormatted() }}</div> </div> <!-- Trade Details Summary --> <div class="border-border/60 bg-muted/20 text-muted-foreground space-y-1.5 rounded-md border p-2.5 text-xs" > <div class="flex items-center justify-between"> <span>Exchange Rate</span> <span class="text-foreground font-medium tabular-nums">{{ exchangeRateFormatted() }}</span> </div> <div class="flex items-center justify-between"> <span class="flex items-center gap-1"> <lucide-icon [img]="Fuel" class="text-muted-foreground size-3" /> Est. Network Gas </span> <span class="text-foreground font-medium tabular-nums">~$3.40 (Fast)</span> </div> <div class="flex items-center justify-between"> <span>Slippage Tolerance</span> <span class="text-foreground font-medium tabular-nums">{{ slippage() }}%</span> </div> <div class="flex items-center justify-between"> <span>Price Impact</span> <span class="text-success text-success font-medium tabular-nums">< 0.01%</span> </div> </div> <!-- Execute Swap Button --> <button ui-button class="w-full font-semibold shadow-xs" size="lg" [disabled]="swapDisabled" (click)="handleExecuteSwap()" > @if (isSwapping()) { <lucide-icon [img]="RefreshCw" class="size-4 animate-spin" /> } @else if (swapSuccess()) { <lucide-icon [img]="Check" class="text-success size-4" /> } @if (isSwapping()) { <span>Routing Transaction...</span> } @else if (swapSuccess()) { <span>Swap Confirmed!</span> } @else { <span>Swap {{ fromAsset().symbol }} for {{ toAsset().symbol }}</span> } </button> <p class="text-muted-foreground text-center text-xs"> Direct routing via Uniswap v3 & Curve liquidity pools. </p> </div> </div> <!-- Security & Vault Status Summary --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-2"> <h3 ui-card-title class="text-foreground flex items-center gap-1.5 text-sm font-semibold"> <lucide-icon [img]="ShieldCheck" class="text-success size-4" /> Vault Security & Policies </h3> </div> <div ui-card-content class="text-muted-foreground space-y-3 text-xs"> <div class="flex items-center justify-between"> <span>Threshold Policy</span> <span class="text-foreground font-mono font-medium">3 / 5 Approvals</span> </div> <div ui-separator class="my-1"></div> <div class="flex items-center justify-between"> <span>Daily Spend Limit</span> <span class="text-foreground font-medium tabular-nums">$500,000.00</span> </div> <div ui-separator class="my-1"></div> <div class="flex items-center justify-between"> <span>Hardware Key Modules</span> <span ui-badge variant="outline" class="border-success/30 text-success text-xs font-normal"> 5 Ledger HSMs Active </span> </div> </div> </div> </div> </div> </div> `, }) export class UiCryptoWalletPortfolioComponent { protected readonly ArrowDownLeft = ArrowDownLeft protected readonly ArrowDownUp = ArrowDownUp protected readonly ArrowLeftRight = ArrowLeftRight protected readonly ArrowUpRight = ArrowUpRight protected readonly Check = Check protected readonly Copy = Copy protected readonly Fuel = Fuel protected readonly RefreshCw = RefreshCw protected readonly ShieldCheck = ShieldCheck protected readonly TrendingDown = TrendingDown protected readonly TrendingUp = TrendingUp protected readonly Wallet = Wallet protected readonly assets = cryptoAssets protected readonly shortAddress = walletShortAddress protected readonly slippageOptions = slippageOptions @Input('class') className?: string readonly copied = signal(false) readonly fromTokenSymbol = signal('ETH') readonly toTokenSymbol = signal('USDC') readonly payAmount = signal('1.5') readonly slippage = signal('0.5') readonly isSwapping = signal(false) readonly swapSuccess = signal(false) private copyTimer: ReturnType<typeof setTimeout> | undefined private swapTimers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => { clearTimeout(this.copyTimer) for (const t of this.swapTimers) clearTimeout(t) }) } // Quick Swap State readonly tokenMap = computed(() => { const map: Record<string, CryptoAsset> = {} for (const asset of cryptoAssets) map[asset.symbol] = asset return map }) readonly fromAsset = computed(() => this.tokenMap()[this.fromTokenSymbol()] ?? cryptoAssets[0]!) readonly toAsset = computed(() => this.tokenMap()[this.toTokenSymbol()] ?? cryptoAssets[3]!) readonly calculatedReceive = computed(() => { const amount = parseFloat(this.payAmount()) || 0 if (amount <= 0) return '0.00' const fromFiat = amount * this.fromAsset().rawPrice const receiveUnits = fromFiat / this.toAsset().rawPrice if (this.toAsset().rawPrice >= 100) return receiveUnits.toFixed(4) return receiveUnits.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }) readonly fiatPayFormatted = computed(() => ((parseFloat(this.payAmount()) || 0) * this.fromAsset().rawPrice).toLocaleString('en-US', { style: 'currency', currency: 'USD', }), ) readonly fiatReceiveFormatted = computed(() => ((parseFloat(this.payAmount()) || 0) * this.fromAsset().rawPrice).toLocaleString('en-US', { style: 'currency', currency: 'USD', }), ) readonly exchangeRateFormatted = computed(() => { const rate = this.fromAsset().rawPrice / this.toAsset().rawPrice const formattedRate = rate > 100 ? rate.toLocaleString('en-US', { maximumFractionDigits: 2 }) : rate.toLocaleString('en-US', { maximumFractionDigits: 6 }) return `1 ${this.fromAsset().symbol} ≈ ${formattedRate} ${this.toAsset().symbol}` }) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } get swapDisabled(): boolean { return this.isSwapping() || (parseFloat(this.payAmount()) || 0) <= 0 } trendClass(asset: CryptoAsset): string { return cn( 'flex items-center justify-end gap-0.5 text-xs font-medium tabular-nums', asset.isPositive ? 'text-success' : 'text-destructive', ) } slippageClass(opt: string): string { return cn( 'focus-visible:ring-ring min-h-6 rounded px-1.5 py-0.5 font-mono text-xs transition-colors focus-visible:ring-1 focus-visible:outline-none', this.slippage() === opt ? 'bg-primary text-primary-foreground font-semibold' : 'text-muted-foreground hover:text-foreground bg-muted/60', ) } async copyAddress(): Promise<void> { try { await navigator.clipboard.writeText(walletFullAddress) this.copied.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copied.set(false), 2000) } catch { // Clipboard fallback } } selectTradePair(symbol: string): void { this.fromTokenSymbol.set(symbol) this.toTokenSymbol.set(symbol === 'USDC' ? 'ETH' : 'USDC') } flipTokens(): void { const from = this.fromTokenSymbol() this.fromTokenSymbol.set(this.toTokenSymbol()) this.toTokenSymbol.set(from) } setMaxPay(): void { this.payAmount.set(String(this.fromAsset().rawBalance)) } handleExecuteSwap(): void { this.isSwapping.set(true) this.swapTimers.push( setTimeout(() => { this.isSwapping.set(false) this.swapSuccess.set(true) this.swapTimers.push(setTimeout(() => this.swapSuccess.set(false), 3000)) }, 1200), ) } }src/app/components/blocks/crypto-wallet-portfolio/index.ts0.2 kB
export { UiCryptoWalletPortfolioComponent, cryptoAssets, walletFullAddress, walletShortAddress, type CryptoAsset, } from './crypto-wallet-portfolio.component'
Raw manifest:https://uipkge.dev/r/angular/crypto-wallet-portfolio.json