From 8b31b3365751a0910d8e05b8d2862a5c0c66defc Mon Sep 17 00:00:00 2001 From: jeremy-babylonlabs Date: Mon, 6 Oct 2025 20:53:15 +0700 Subject: [PATCH 1/4] feat(packages): pegin flow --- .../components/ActivityList/ActivityList.tsx | 36 +++ routes/vault/src/api/getVaultProviders.ts | 80 ++++++ routes/vault/src/api/index.ts | 7 + routes/vault/src/assets/index.ts | 3 + .../vault/src/components/PeginFlow/index.ts | 1 + .../components/PeginFlow/usePeginFlowState.ts | 85 ++++++ .../src/components/modals/PeginModal.tsx | 245 ++++++++++++++++++ .../src/components/modals/PeginSignModal.tsx | 116 +++++++++ .../components/modals/PeginSuccessModal.tsx | 60 +++++ routes/vault/src/components/modals/index.ts | 3 + routes/vault/src/hooks/index.ts | 4 +- routes/vault/src/hooks/usePeginForm.ts | 135 ++++++++++ routes/vault/src/hooks/useVaultProviders.ts | 17 ++ .../mascot-smile-expression-full-body.png | Bin 0 -> 30249 bytes .../public/mascot-smile-expression.png | Bin 0 -> 34958 bytes 15 files changed, 791 insertions(+), 1 deletion(-) create mode 100644 routes/vault/src/api/getVaultProviders.ts create mode 100644 routes/vault/src/api/index.ts create mode 100644 routes/vault/src/components/PeginFlow/index.ts create mode 100644 routes/vault/src/components/PeginFlow/usePeginFlowState.ts create mode 100644 routes/vault/src/components/modals/PeginModal.tsx create mode 100644 routes/vault/src/components/modals/PeginSignModal.tsx create mode 100644 routes/vault/src/components/modals/PeginSuccessModal.tsx create mode 100644 routes/vault/src/hooks/usePeginForm.ts create mode 100644 routes/vault/src/hooks/useVaultProviders.ts create mode 100644 services/simple-staking/public/mascot-smile-expression-full-body.png create mode 100644 services/simple-staking/public/mascot-smile-expression.png diff --git a/packages/babylon-core-ui/src/components/ActivityList/ActivityList.tsx b/packages/babylon-core-ui/src/components/ActivityList/ActivityList.tsx index cedb9483..19e6827c 100644 --- a/packages/babylon-core-ui/src/components/ActivityList/ActivityList.tsx +++ b/packages/babylon-core-ui/src/components/ActivityList/ActivityList.tsx @@ -17,13 +17,49 @@ import type { PropsWithChildren } from "react"; export interface ActivityListProps { onNewItem?: () => void; className?: string; + isEmpty?: boolean; + isConnected?: boolean; } export function ActivityList({ onNewItem, className, children, + isEmpty = false, + isConnected = false, }: PropsWithChildren) { + // Show empty state when connected but no activities + if (isEmpty && isConnected) { + return ( + +
+ Babylon mascot +
+

+ Supply Collateral BTC Trustlessly +

+

+ Enter the amount of BTC you want to deposit and select a provider to secure it. + Your deposit will appear here once confirmed. +

+
+ + + +
+
+ ); + } + return (
diff --git a/routes/vault/src/api/getVaultProviders.ts b/routes/vault/src/api/getVaultProviders.ts new file mode 100644 index 00000000..dde3c9db --- /dev/null +++ b/routes/vault/src/api/getVaultProviders.ts @@ -0,0 +1,80 @@ +/** + * Vault Provider API + * + * This module handles fetching vault provider data. + * Currently returns mock data, but structured to support GraphQL integration. + */ + +export interface VaultProvider { + id: string; + name: string; + icon: string; + apy?: number; + tvl?: string; + description?: string; +} + +export interface VaultProvidersResponse { + providers: VaultProvider[]; +} + +/** + * Fetch vault providers from backend + * TODO: Implement GraphQL query when backend is ready + * + * @returns Promise + */ +export async function getVaultProviders(): Promise { + // TODO: Replace with actual GraphQL endpoint + // Example GraphQL query: + // query GetVaultProviders { + // vaultProviders { + // id + // name + // icon + // apy + // tvl + // description + // } + // } + + // Mock data - will be replaced by GraphQL query + const mockProviders: VaultProvider[] = [ + { + id: "ironclad", + name: "Ironclad BTC", + icon: "/icons/ironclad.svg", + apy: 8.5, + tvl: "1.2M", + description: "Secure Bitcoin custody with institutional-grade security" + }, + { + id: "atlas", + name: "Atlas Custody", + icon: "/icons/atlas.svg", + apy: 7.8, + tvl: "2.5M", + description: "Regulated custody solution for digital assets" + }, + { + id: "stonewall", + name: "Stonewall Capital", + icon: "/icons/stonewall.svg", + apy: 9.2, + tvl: "850K", + description: "High-yield Bitcoin vault provider" + }, + { + id: "redwood", + name: "Redwood BTC", + icon: "/icons/redwood.svg", + apy: 8.0, + tvl: "1.8M", + description: "Enterprise-grade Bitcoin vault infrastructure" + }, + ]; + + return { + providers: mockProviders, + }; +} \ No newline at end of file diff --git a/routes/vault/src/api/index.ts b/routes/vault/src/api/index.ts new file mode 100644 index 00000000..f38c1f63 --- /dev/null +++ b/routes/vault/src/api/index.ts @@ -0,0 +1,7 @@ +/** + * API Module + * + * Centralized exports for all API functions + */ + +export * from './getVaultProviders'; \ No newline at end of file diff --git a/routes/vault/src/assets/index.ts b/routes/vault/src/assets/index.ts index aea86a43..4a1dba68 100644 --- a/routes/vault/src/assets/index.ts +++ b/routes/vault/src/assets/index.ts @@ -1,5 +1,8 @@ // Asset exports for vault application +// Bitcoin icon as data URI - Orange bitcoin logo +export const bitcoinIcon = "data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23FF7C2A' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z'/%3E%3C/svg%3E"; + // USDC icon as data URI - Simple circle design with USDC branding export const usdcIcon = 'data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="20" cy="20" r="20" fill="%232775CA"/%3E%3Cpath d="M24.0001 17.3999C24.0001 15.6399 22.8001 14.7999 20.4001 14.5999V12.3999H18.8001V14.5599C18.3601 14.5599 17.9201 14.5599 17.4801 14.5999V12.3999H15.8801V14.5999C15.5601 14.5999 15.2401 14.6399 14.9201 14.6399H13.2001V16.3599H14.4401C14.8801 16.3599 15.0801 16.5999 15.0801 16.9599V23.0399C15.0801 23.3999 14.8801 23.6399 14.4401 23.6399H13.2001V25.7599L14.8401 25.7999C15.1601 25.7999 15.4801 25.7999 15.8001 25.8399V28.0399H17.4001V25.8799C17.8401 25.8799 18.2801 25.9199 18.7201 25.9199V28.0799H20.3201V25.8799C23.2001 25.7199 24.8001 24.5199 24.8001 22.3599C24.8001 20.7999 24.0001 19.8799 22.5601 19.4799C23.5201 19.0799 24.0001 18.3599 24.0001 17.3999ZM18.7201 21.1999V23.5199C17.7201 23.4399 17.2001 23.3999 16.6801 23.3599V21.1999C17.2001 21.1599 17.7201 21.1199 18.7201 21.1999ZM20.3201 16.7199C21.2401 16.7999 21.7601 16.8399 22.3201 16.9199V19.0399C21.7601 18.9599 21.2401 18.9199 20.3201 18.8399V16.7199ZM20.3201 23.5599V21.1599C21.3201 21.2399 21.8401 21.2799 22.4001 21.3599C22.6001 22.3599 21.8401 23.1999 20.3201 23.5599Z" fill="white"/%3E%3C/svg%3E'; diff --git a/routes/vault/src/components/PeginFlow/index.ts b/routes/vault/src/components/PeginFlow/index.ts new file mode 100644 index 00000000..44dfa4fd --- /dev/null +++ b/routes/vault/src/components/PeginFlow/index.ts @@ -0,0 +1 @@ +export { usePeginFlowState } from './usePeginFlowState'; diff --git a/routes/vault/src/components/PeginFlow/usePeginFlowState.ts b/routes/vault/src/components/PeginFlow/usePeginFlowState.ts new file mode 100644 index 00000000..9270c67d --- /dev/null +++ b/routes/vault/src/components/PeginFlow/usePeginFlowState.ts @@ -0,0 +1,85 @@ +import { useState, useCallback, useMemo } from "react"; +import { useChainConnector } from "@babylonlabs-io/wallet-connector"; +import type { Hex } from "viem"; + +export function usePeginFlowState() { + const ethConnector = useChainConnector('ETH'); + const btcConnector = useChainConnector('BTC'); + + // Get BTC address from connector + const btcAddress = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (btcConnector as any)?.connectedWallet?.account?.address as string | undefined; + }, [btcConnector]); + + // Get ETH address from connector + const connectedAddress = useMemo(() => { + const address = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (ethConnector as any)?.connectedWallet?.account?.address || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (ethConnector as any)?.connectedWallet?.accounts?.[0]?.address + ) as Hex | undefined; + return address; + }, [ethConnector]); + + // Hardcoded BTC balance (in satoshis) - TODO: Replace with real wallet balance + const btcBalanceSat = 500000000; // 5 BTC + + // Modal states + const [peginModalOpen, setPeginModalOpen] = useState(false); + const [peginSignModalOpen, setPeginSignModalOpen] = useState(false); + const [peginSuccessModalOpen, setPeginSuccessModalOpen] = useState(false); + + // Peg-in flow data + const [peginAmount, setPeginAmount] = useState(0); + const [selectedProviders, setSelectedProviders] = useState([]); + + // Start the peg-in flow + const handleNewBorrow = useCallback(() => { + setPeginModalOpen(true); + }, []); + + // Handle peg-in click from PeginModal + const handlePeginClick = useCallback((amount: number, providers: string[]) => { + console.log("Peg-in clicked:", { amount, providers }); + setPeginAmount(amount); + setSelectedProviders(providers); + setPeginModalOpen(false); + setPeginSignModalOpen(true); + }, []); + + // Handle signing success + const handlePeginSignSuccess = useCallback(() => { + setPeginSignModalOpen(false); + setPeginSuccessModalOpen(true); + }, []); + + // Handle success modal close + const handlePeginSuccessClose = useCallback(() => { + setPeginSuccessModalOpen(false); + setPeginAmount(0); + setSelectedProviders([]); + }, []); + + return { + // Wallet data + connectedAddress, + btcAddress, + btcBalanceSat, + // Modal states + peginModalOpen, + peginSignModalOpen, + peginSuccessModalOpen, + // Peg-in data + peginAmount, + selectedProviders, + // Actions + handleNewBorrow, + handlePeginClick, + handlePeginSignSuccess, + handlePeginSuccessClose, + setPeginModalOpen, + setPeginSignModalOpen, + }; +} diff --git a/routes/vault/src/components/modals/PeginModal.tsx b/routes/vault/src/components/modals/PeginModal.tsx new file mode 100644 index 00000000..0fcfb2dd --- /dev/null +++ b/routes/vault/src/components/modals/PeginModal.tsx @@ -0,0 +1,245 @@ +import { + Button, + ResponsiveDialog, + DialogBody, + DialogFooter, + DialogHeader, + Text, + AmountItem, + SubSection, + Loader, +} from "@babylonlabs-io/core-ui"; +import { useState, useMemo } from "react"; +import { bitcoinIcon } from "../../assets"; +import { useVaultProviders } from "../../hooks/useVaultProviders"; +import { usePeginForm } from "../../hooks/usePeginForm"; +import type { VaultProvider } from "../../api"; + +interface PeginModalProps { + open: boolean; + onClose: () => void; + onPegIn: (amount: number, providers: string[]) => void; + btcBalance?: number; // BTC balance in satoshis +} + +export function PeginModal({ open, onClose, onPegIn, btcBalance = 0 }: PeginModalProps) { + // Local state for form inputs + const [amount, setAmount] = useState(""); + const [selectedProviders, setSelectedProviders] = useState([]); + + // Fetch vault providers + const { data: vaultProvidersData, isLoading: isLoadingProviders, error: providersError } = useVaultProviders(); + const vaultProviders = vaultProvidersData?.providers || []; + + // Hardcoded BTC price - TODO: Replace with real price feed from API + const btcPrice = 95000; // $95,000 per BTC + + // Use deposit form hook for validation and business logic + const { + btcBalanceFormatted, + calculateUsdValue, + validateAmount, + validateProviders, + validateForm, + getMaxBalance, + coinName, + } = usePeginForm({ + btcBalance, + btcPrice, + coinName: 'BTC', + displayUSD: true, + }); + + // Parse amount as number + const amountNum = useMemo(() => { + const parsed = parseFloat(amount || "0"); + return isNaN(parsed) ? 0 : parsed; + }, [amount]); + + // Calculate USD equivalent + const amountUsd = useMemo(() => calculateUsdValue(amountNum), [amountNum, calculateUsdValue]); + + // Get validation states from hook + const amountValidation = useMemo(() => validateAmount(amountNum), [amountNum, validateAmount]); + const providersValidation = useMemo(() => validateProviders(selectedProviders), [selectedProviders, validateProviders]); + const isValid = useMemo(() => validateForm(amountNum, selectedProviders), [amountNum, selectedProviders, validateForm]); + + // Determine what error to show + const showAmountError = amount !== "" && !amountValidation.valid; + const showProvidersError = amount !== "" && amountNum > 0 && !providersValidation.valid; + + // Handler: Toggle provider selection + const handleToggleProvider = (providerId: string) => { + setSelectedProviders((prev) => + prev.includes(providerId) + ? prev.filter((id) => id !== providerId) + : [...prev, providerId] + ); + }; + + // Handler: Amount input change + const handleAmountChange = (e: React.ChangeEvent) => { + setAmount(e.target.value); + }; + + // Handler: Balance click to auto-fill max amount + const handleBalanceClick = () => { + const maxBalance = getMaxBalance(); + if (maxBalance > 0) { + setAmount(maxBalance.toString()); + } + }; + + // Handler: Peg-in button click + const handlePegIn = () => { + if (isValid) { + console.log("Peg-in:", { amount: amountNum, providers: selectedProviders }); + onPegIn(amountNum, selectedProviders); + } + }; + + // Handler: Prevent arrow keys from changing number input + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowUp" || e.key === "ArrowDown") { + e.preventDefault(); + } + }; + + // Handler: Reset state when modal closes + const handleClose = () => { + setAmount(""); + setSelectedProviders([]); + onClose(); + }; + + return ( + + + + + {/* Bitcoin Amount Section */} +
+ + Bitcoin + + + + + {/* Clickable Balance Display */} + + + {/* Error Messages */} + {showAmountError && amountValidation.error && ( + + {amountValidation.error} + + )} + {showProvidersError && providersValidation.error && ( + + {providersValidation.error} + + )} + +
+ + {/* Vault Provider Selection Section */} +
+
+ + Select Vault Providers + + + Choose one or more providers to secure your BTC + +
+ + {isLoadingProviders ? ( +
+ +
+ ) : providersError ? ( +
+ + Failed to load vault providers. Please try again. + +
+ ) : ( +
+ {vaultProviders.map((provider: VaultProvider) => { + const isSelected = selectedProviders.includes(provider.id); + return ( +
+
+
+ + {provider.name.charAt(0)} + +
+
+ + {provider.name} + + {provider.apy && ( + + APY: {provider.apy}% + + )} +
+
+ +
+ ); + })} +
+ )} +
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/routes/vault/src/components/modals/PeginSignModal.tsx b/routes/vault/src/components/modals/PeginSignModal.tsx new file mode 100644 index 00000000..1c986e88 --- /dev/null +++ b/routes/vault/src/components/modals/PeginSignModal.tsx @@ -0,0 +1,116 @@ +import { + Button, + DialogBody, + DialogFooter, + DialogHeader, + Loader, + ResponsiveDialog, + Step, + Text, +} from "@babylonlabs-io/core-ui"; +import { useEffect, useState } from "react"; + +interface PeginSignModalProps { + open: boolean; + onClose: () => void; + onSuccess: () => void; + amount: number; + selectedProviders: string[]; +} + +/** + * PeginSignModal - Multi-step signing modal for deposit flow + * + * Shows 4 steps that auto-progress with 2-second delays: + * 1. Sign proof of possession + * 2. Sign & broadcast pegInRequest to Vault Controller + * 3. Validating + * 4. Payout transactions + * + * After all steps complete, triggers onSuccess callback to show success modal. + * + * Note: This is hardcoded for UI demonstration only. + * Real implementation would integrate with wallet signing. + */ +export function PeginSignModal({ + open, + onClose, + onSuccess, +}: PeginSignModalProps) { + const [currentStep, setCurrentStep] = useState(1); + const [processing, setProcessing] = useState(false); + + // Auto-progress through steps with 2-second delays (hardcoded for UI demo) + useEffect(() => { + if (!open || currentStep > 4) return; + + setProcessing(true); + + const timer = setTimeout(() => { + if (currentStep === 4) { + // Last step complete, trigger success modal + setProcessing(false); + onSuccess(); + } else { + // Move to next step + setCurrentStep((prev) => prev + 1); + } + }, 2000); // 2 second delay per step + + return () => clearTimeout(timer); + }, [open, currentStep, onSuccess]); + + // Reset state when modal closes + useEffect(() => { + if (!open) { + setCurrentStep(1); + setProcessing(false); + } + }, [open]); + + return ( + + + + + + Please sign the following messages + + +
+ + Step 1: Sign proof of possession + + + Step 2: Sign & broadcast pegInRequest to Vault Controller + + + Step 3: Validating + + + Step 4: Payout transactions + +
+
+ + + + +
+ ); +} diff --git a/routes/vault/src/components/modals/PeginSuccessModal.tsx b/routes/vault/src/components/modals/PeginSuccessModal.tsx new file mode 100644 index 00000000..a51e3be6 --- /dev/null +++ b/routes/vault/src/components/modals/PeginSuccessModal.tsx @@ -0,0 +1,60 @@ +import { + Button, + DialogBody, + DialogFooter, + Heading, + ResponsiveDialog, + Text, +} from "@babylonlabs-io/core-ui"; + +interface PeginSuccessModalProps { + open: boolean; + onClose: () => void; + amount: number; +} + +/** + * PeginSuccessModal - Success celebration modal after deposit completion + * + * Displays: + * - Mascot image (celebrating) + * - "BTC Peg-in Successful" heading + * - Confirmation message with wait time (~5 hours) + * - "Done" button to close + */ +export function PeginSuccessModal({ + open, + onClose, +}: PeginSuccessModalProps) { + return ( + + + Success mascot + + + BTC Peg-in Successful + + + + Your deposit has been recorded and is now awaiting confirmation on + the Bitcoin network. This usually takes up to 5 hours. + + + + + + + + ); +} diff --git a/routes/vault/src/components/modals/index.ts b/routes/vault/src/components/modals/index.ts index 3bba0bd4..1c5c0ec0 100644 --- a/routes/vault/src/components/modals/index.ts +++ b/routes/vault/src/components/modals/index.ts @@ -2,3 +2,6 @@ export { BorrowModal } from "./BorrowModal"; export { BorrowSignModal } from "./BorrowSignModal"; export { BorrowSuccessModal } from "./BorrowSuccessModal"; +export { PeginModal } from "./PeginModal"; +export { PeginSignModal } from "./PeginSignModal"; +export { PeginSuccessModal } from "./PeginSuccessModal"; diff --git a/routes/vault/src/hooks/index.ts b/routes/vault/src/hooks/index.ts index 90fa68cb..26f0bf91 100644 --- a/routes/vault/src/hooks/index.ts +++ b/routes/vault/src/hooks/index.ts @@ -3,4 +3,6 @@ */ export { usePeginRequests } from './usePeginRequests'; -export type { UsePeginRequestsResult } from './usePeginRequests'; \ No newline at end of file +export type { UsePeginRequestsResult } from './usePeginRequests'; +export { useVaultProviders } from './useVaultProviders'; +export { usePeginForm } from './usePeginForm'; \ No newline at end of file diff --git a/routes/vault/src/hooks/usePeginForm.ts b/routes/vault/src/hooks/usePeginForm.ts new file mode 100644 index 00000000..65d41031 --- /dev/null +++ b/routes/vault/src/hooks/usePeginForm.ts @@ -0,0 +1,135 @@ +import { useMemo, useCallback } from 'react'; +import { object, number, array, string } from 'yup'; + +// Helper function to convert satoshis to BTC +const satoshiToBtc = (satoshi: number): number => { + return satoshi / 100000000; +}; + +// Helper function to format number +const formatNumber = (value: any): number => { + if (typeof value === 'number') return value; + const parsed = parseFloat(value); + return isNaN(parsed) ? 0 : parsed; +}; + +// Helper function to validate decimal points +const validateDecimalPoints = (value: any): boolean => { + if (!value) return true; + const str = String(value); + const decimalIndex = str.indexOf('.'); + if (decimalIndex === -1) return true; + const decimals = str.slice(decimalIndex + 1).length; + return decimals <= 8; +}; + +interface UsePeginFormParams { + btcBalance: number; // in satoshis + btcPrice: number; + coinName?: string; + displayUSD?: boolean; +} + +export function usePeginForm({ + btcBalance, + btcPrice, + coinName = 'BTC', + displayUSD = true, +}: UsePeginFormParams) { + // Convert balance from satoshis to BTC + const btcBalanceFormatted = useMemo( + () => satoshiToBtc(btcBalance), + [btcBalance] + ); + + // Validation schema (following simple-staking pattern) + const validationSchema = useMemo( + () => + object().shape({ + amount: number() + .transform(formatNumber) + .typeError('Peg-in amount must be a valid number.') + .required('Peg-in amount is required.') + .moreThan(0, 'Peg-in amount must be greater than 0.') + .max( + btcBalanceFormatted, + `Peg-in amount exceeds your balance (${satoshiToBtc(btcBalance).toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 8 })} ${coinName})!` + ) + .test( + 'decimal-points', + 'Peg-in amount must have no more than 8 decimal points.', + validateDecimalPoints + ), + + selectedProviders: array() + .of(string().required()) + .required('Please select at least one vault provider.') + .min(1, 'Please select at least one vault provider.'), + }), + [btcBalance, btcBalanceFormatted, coinName] + ); + + // Calculate USD equivalent + const calculateUsdValue = useCallback( + (amount: number): string => { + if (!displayUSD || !btcPrice || amount === 0) return ''; + const usdValue = amount * btcPrice; + return `$${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + [btcPrice, displayUSD] + ); + + // Validate amount + const validateAmount = useCallback( + (amount: number): { valid: boolean; error?: string } => { + if (amount <= 0) { + return { valid: false, error: 'Peg-in amount must be greater than 0.' }; + } + if (amount > btcBalanceFormatted) { + return { valid: false, error: 'Amount exceeds available balance' }; + } + if (!validateDecimalPoints(amount)) { + return { valid: false, error: 'Peg-in amount must have no more than 8 decimal points.' }; + } + return { valid: true }; + }, + [btcBalanceFormatted] + ); + + // Validate providers + const validateProviders = useCallback( + (providers: string[]): { valid: boolean; error?: string } => { + if (providers.length === 0) { + return { valid: false, error: 'Please select at least one vault provider' }; + } + return { valid: true }; + }, + [] + ); + + // Validate entire form + const validateForm = useCallback( + (amount: number, providers: string[]): boolean => { + const amountValidation = validateAmount(amount); + const providersValidation = validateProviders(providers); + return amountValidation.valid && providersValidation.valid; + }, + [validateAmount, validateProviders] + ); + + // Handler: Auto-fill max balance + const getMaxBalance = useCallback((): number => { + return btcBalanceFormatted; + }, [btcBalanceFormatted]); + + return { + validationSchema, + btcBalanceFormatted, + calculateUsdValue, + validateAmount, + validateProviders, + validateForm, + getMaxBalance, + coinName, + }; +} diff --git a/routes/vault/src/hooks/useVaultProviders.ts b/routes/vault/src/hooks/useVaultProviders.ts new file mode 100644 index 00000000..ca958512 --- /dev/null +++ b/routes/vault/src/hooks/useVaultProviders.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { getVaultProviders } from '../api'; + +const FIVE_MINUTES = 5 * 60 * 1000; + +export const VAULT_PROVIDERS_KEY = 'VAULT_PROVIDERS'; + +/** + * Hook to fetch vault providers + */ +export const useVaultProviders = () => { + return useQuery({ + queryKey: [VAULT_PROVIDERS_KEY], + queryFn: getVaultProviders, + staleTime: FIVE_MINUTES, + }); +}; diff --git a/services/simple-staking/public/mascot-smile-expression-full-body.png b/services/simple-staking/public/mascot-smile-expression-full-body.png new file mode 100644 index 0000000000000000000000000000000000000000..68a89f820b9df3240990076140f6e495338e4f27 GIT binary patch literal 30249 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 V+$MbBpvkV^q{S7)s(u;<{vS(!Tz>!n literal 0 HcmV?d00001 diff --git a/services/simple-staking/public/mascot-smile-expression.png b/services/simple-staking/public/mascot-smile-expression.png new file mode 100644 index 0000000000000000000000000000000000000000..70370b40c395ae05d8b81993ae208c67a3e88f23 GIT binary patch literal 34958 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�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

rP+`F6#EeO>sPj!`|s# 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 Date: Mon, 6 Oct 2025 21:43:01 +0700 Subject: [PATCH 2/4] feat(packages): pegin flow --- package-lock.json | 18 ++- package.json | 3 +- .../sections/ActivityCard/ActivityCard.tsx | 7 + .../components/PeginFlow/usePeginFlowState.ts | 37 ++--- .../src/components/modals/PeginModal.tsx | 4 +- .../src/components/modals/PeginSignModal.tsx | 2 +- .../components/modals/PeginSuccessModal.tsx | 4 +- routes/vault/src/hooks/index.ts | 3 +- routes/vault/src/hooks/usePeginStorage.ts | 129 +++++++++++++++ routes/vault/src/mockData/vaultActivities.ts | 3 + routes/vault/src/storage/index.ts | 6 +- routes/vault/src/storage/peginStorage.ts | 151 ++++++++++++++++++ 12 files changed, 330 insertions(+), 37 deletions(-) create mode 100644 routes/vault/src/hooks/usePeginStorage.ts create mode 100644 routes/vault/src/storage/peginStorage.ts diff --git a/package-lock.json b/package-lock.json index c690f967..1ab11251 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "dependencies": { "@morpho-org/blue-sdk": "^5.0.0", "@morpho-org/blue-sdk-viem": "^4.0.0", - "@tanstack/react-query": "^5.90.2" + "@tanstack/react-query": "^5.90.2", + "usehooks-ts": "^3.1.1" }, "devDependencies": { "@commitlint/cli": "^19.8.0", @@ -32263,6 +32264,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", diff --git a/package.json b/package.json index d52da1aa..8e61876f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "dependencies": { "@morpho-org/blue-sdk": "^5.0.0", "@morpho-org/blue-sdk-viem": "^4.0.0", - "@tanstack/react-query": "^5.90.2" + "@tanstack/react-query": "^5.90.2", + "usehooks-ts": "^3.1.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "4.52.4" diff --git a/packages/babylon-core-ui/src/widgets/sections/ActivityCard/ActivityCard.tsx b/packages/babylon-core-ui/src/widgets/sections/ActivityCard/ActivityCard.tsx index b177f4d2..991f77d3 100644 --- a/packages/babylon-core-ui/src/widgets/sections/ActivityCard/ActivityCard.tsx +++ b/packages/babylon-core-ui/src/widgets/sections/ActivityCard/ActivityCard.tsx @@ -39,6 +39,7 @@ export interface ActivityCardData { label: string; items: ActivityListItemData[]; }[]; + warning?: React.ReactNode; primaryAction?: ActivityCardActionButton; secondaryActions?: ActivityCardActionButton[]; } @@ -72,6 +73,12 @@ export function ActivityCard({ data, className }: ActivityCardProps) { {data.secondaryActions && data.secondaryActions.length > 0 && ( )} + + {data.warning && ( +

+ {data.warning} +
+ )}
{data.primaryAction && ( diff --git a/routes/vault/src/components/PeginFlow/usePeginFlowState.ts b/routes/vault/src/components/PeginFlow/usePeginFlowState.ts index 9270c67d..a5238f38 100644 --- a/routes/vault/src/components/PeginFlow/usePeginFlowState.ts +++ b/routes/vault/src/components/PeginFlow/usePeginFlowState.ts @@ -1,28 +1,6 @@ -import { useState, useCallback, useMemo } from "react"; -import { useChainConnector } from "@babylonlabs-io/wallet-connector"; -import type { Hex } from "viem"; +import { useState, useCallback } from "react"; export function usePeginFlowState() { - const ethConnector = useChainConnector('ETH'); - const btcConnector = useChainConnector('BTC'); - - // Get BTC address from connector - const btcAddress = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (btcConnector as any)?.connectedWallet?.account?.address as string | undefined; - }, [btcConnector]); - - // Get ETH address from connector - const connectedAddress = useMemo(() => { - const address = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (ethConnector as any)?.connectedWallet?.account?.address || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (ethConnector as any)?.connectedWallet?.accounts?.[0]?.address - ) as Hex | undefined; - return address; - }, [ethConnector]); - // Hardcoded BTC balance (in satoshis) - TODO: Replace with real wallet balance const btcBalanceSat = 500000000; // 5 BTC @@ -49,10 +27,15 @@ export function usePeginFlowState() { setPeginSignModalOpen(true); }, []); - // Handle signing success - const handlePeginSignSuccess = useCallback(() => { + // Handle signing success - accepts callback for parent to handle storage + const handlePeginSignSuccess = useCallback((onSuccess?: () => void) => { setPeginSignModalOpen(false); setPeginSuccessModalOpen(true); + + // Call parent callback if provided + if (onSuccess) { + onSuccess(); + } }, []); // Handle success modal close @@ -64,8 +47,6 @@ export function usePeginFlowState() { return { // Wallet data - connectedAddress, - btcAddress, btcBalanceSat, // Modal states peginModalOpen, @@ -82,4 +63,4 @@ export function usePeginFlowState() { setPeginModalOpen, setPeginSignModalOpen, }; -} +} \ No newline at end of file diff --git a/routes/vault/src/components/modals/PeginModal.tsx b/routes/vault/src/components/modals/PeginModal.tsx index 0fcfb2dd..0a4578db 100644 --- a/routes/vault/src/components/modals/PeginModal.tsx +++ b/routes/vault/src/components/modals/PeginModal.tsx @@ -115,7 +115,7 @@ export function PeginModal({ open, onClose, onPegIn, btcBalance = 0 }: PeginModa return ( @@ -237,7 +237,7 @@ export function PeginModal({ open, onClose, onPegIn, btcBalance = 0 }: PeginModa onClick={handlePegIn} className="w-full text-sm sm:text-base" > - Peg-in + Deposit diff --git a/routes/vault/src/components/modals/PeginSignModal.tsx b/routes/vault/src/components/modals/PeginSignModal.tsx index 1c986e88..8a822ef7 100644 --- a/routes/vault/src/components/modals/PeginSignModal.tsx +++ b/routes/vault/src/components/modals/PeginSignModal.tsx @@ -71,7 +71,7 @@ export function PeginSignModal({ return ( diff --git a/routes/vault/src/components/modals/PeginSuccessModal.tsx b/routes/vault/src/components/modals/PeginSuccessModal.tsx index a51e3be6..5f153bc3 100644 --- a/routes/vault/src/components/modals/PeginSuccessModal.tsx +++ b/routes/vault/src/components/modals/PeginSuccessModal.tsx @@ -32,11 +32,11 @@ export function PeginSuccessModal({ Success mascot - BTC Peg-in Successful + BTC Deposit Successful diff --git a/routes/vault/src/hooks/index.ts b/routes/vault/src/hooks/index.ts index 26f0bf91..94b40ccd 100644 --- a/routes/vault/src/hooks/index.ts +++ b/routes/vault/src/hooks/index.ts @@ -5,4 +5,5 @@ export { usePeginRequests } from './usePeginRequests'; export type { UsePeginRequestsResult } from './usePeginRequests'; export { useVaultProviders } from './useVaultProviders'; -export { usePeginForm } from './usePeginForm'; \ No newline at end of file +export { usePeginForm } from './usePeginForm'; +export { usePeginStorage } from './usePeginStorage'; \ No newline at end of file diff --git a/routes/vault/src/hooks/usePeginStorage.ts b/routes/vault/src/hooks/usePeginStorage.ts new file mode 100644 index 00000000..4f456c89 --- /dev/null +++ b/routes/vault/src/hooks/usePeginStorage.ts @@ -0,0 +1,129 @@ +/** + * Hook for managing peg-in local storage + * + * Similar to simple-staking's useDelegationStorage pattern: + * - Merges pending peg-ins from localStorage with confirmed peg-ins from API + * - Automatically removes confirmed peg-ins from localStorage + * - Cleans up old pending peg-ins + */ + +import { useCallback, useEffect, useMemo } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; +import type { VaultActivity } from '../mockData/vaultActivities'; +import { + type PendingPeginRequest, + filterPendingPegins, +} from '../storage/peginStorage'; +import { bitcoinIcon } from '../assets'; + +interface UsePeginStorageParams { + ethAddress: string; + confirmedPegins: VaultActivity[]; // Peg-ins from API/blockchain +} + +export function usePeginStorage({ + ethAddress, + confirmedPegins, +}: UsePeginStorageParams) { + const storageKey = `vault-pending-pegins-${ethAddress}`; + + // Store pending peg-ins in localStorage + const [pendingPegins = [], setPendingPegins] = useLocalStorage< + PendingPeginRequest[] + >(storageKey, []); + + // Create a map of confirmed peg-in IDs for quick lookup + const confirmedPeginMap = useMemo(() => { + return confirmedPegins.reduce( + (acc, pegin) => ({ + ...acc, + [pegin.id]: pegin, + }), + {} as Record, + ); + }, [confirmedPegins]); + + // Sync: Remove pending peg-ins that are now confirmed + useEffect(() => { + if (!ethAddress) return; + + const confirmedIds = Object.keys(confirmedPeginMap); + const filteredPegins = filterPendingPegins(pendingPegins, confirmedIds); + + // Only update if something changed + if (filteredPegins.length !== pendingPegins.length) { + setPendingPegins(filteredPegins); + } + }, [ethAddress, confirmedPeginMap, pendingPegins, setPendingPegins]); + + // Convert pending peg-ins to VaultActivity format + const pendingActivities: VaultActivity[] = useMemo(() => { + return pendingPegins + .filter((pegin: PendingPeginRequest) => !confirmedPeginMap[pegin.id]) // Don't show if already confirmed + .map((pegin: PendingPeginRequest) => ({ + id: pegin.id, + collateral: { + amount: pegin.amount, + symbol: 'BTC', + icon: bitcoinIcon, + }, + status: { + label: 'Pending', + variant: 'pending' as const, + }, + providers: pegin.providers.map((providerId: string) => ({ + id: providerId, + name: providerId, // TODO: Map to actual provider names + icon: undefined, + })), + action: { + label: 'Borrow USDC', + onClick: () => console.log('Borrow from pending peg-in:', pegin.id), + }, + isPending: true, // Flag to show callout message + })); + }, [pendingPegins, confirmedPeginMap]); + + // Merge pending and confirmed activities + const allActivities: VaultActivity[] = useMemo(() => { + return [...pendingActivities, ...confirmedPegins]; + }, [pendingActivities, confirmedPegins]); + + // Add a new pending peg-in + const addPendingPegin = useCallback( + (pegin: Omit) => { + if (!ethAddress) return; + + const newPegin: PendingPeginRequest = { + ...pegin, + timestamp: Date.now(), + status: 'pending', + }; + + setPendingPegins((prev: PendingPeginRequest[]) => [...prev, newPegin]); + }, + [ethAddress, setPendingPegins], + ); + + // Remove a pending peg-in manually + const removePendingPegin = useCallback( + (peginId: string) => { + setPendingPegins((prev: PendingPeginRequest[]) => prev.filter((p: PendingPeginRequest) => p.id !== peginId)); + }, + [setPendingPegins], + ); + + // Clear all pending peg-ins + const clearPendingPegins = useCallback(() => { + setPendingPegins([]); + }, [setPendingPegins]); + + return { + allActivities, + pendingPegins, + pendingActivities, + addPendingPegin, + removePendingPegin, + clearPendingPegins, + }; +} diff --git a/routes/vault/src/mockData/vaultActivities.ts b/routes/vault/src/mockData/vaultActivities.ts index 7258166a..d3c33fea 100644 --- a/routes/vault/src/mockData/vaultActivities.ts +++ b/routes/vault/src/mockData/vaultActivities.ts @@ -58,6 +58,9 @@ export interface VaultActivity { borrowAmount: bigint; active: boolean; }; + // Pending peg-in flags + isPending?: boolean; // Flag to show pending callout message + pendingMessage?: string; // Custom pending message } const bitcoinIconDataUri = "data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23FF7C2A' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z'/%3E%3C/svg%3E"; diff --git a/routes/vault/src/storage/index.ts b/routes/vault/src/storage/index.ts index 4ed79fcb..93a63224 100644 --- a/routes/vault/src/storage/index.ts +++ b/routes/vault/src/storage/index.ts @@ -1 +1,5 @@ -// Local storage utilities for persisting intermediate state (e.g., pending transactions, user preferences) +/** + * Storage utilities + */ + +export * from './peginStorage'; \ No newline at end of file diff --git a/routes/vault/src/storage/peginStorage.ts b/routes/vault/src/storage/peginStorage.ts new file mode 100644 index 00000000..82eb9bcf --- /dev/null +++ b/routes/vault/src/storage/peginStorage.ts @@ -0,0 +1,151 @@ +/** + * Local Storage utilities for pending peg-in transactions + * + * Similar to simple-staking's delegation storage pattern: + * - Store pending peg-ins in localStorage + * - Merge with API data when available + * - Remove from localStorage when confirmed on-chain + */ + +export interface PendingPeginRequest { + id: string; // Unique identifier (BTC tx hash or temporary ID) + btcTxHash?: string; // BTC transaction hash (once available) + amount: string; // BTC amount as string to avoid BigInt serialization issues + providers: string[]; // Selected vault provider IDs + ethAddress: string; // ETH address that initiated the peg-in + btcAddress: string; // BTC address used + timestamp: number; // When the peg-in was initiated + status: 'pending' | 'confirming' | 'confirmed'; +} + +const STORAGE_KEY_PREFIX = 'vault-pending-pegins'; +const MAX_PENDING_DURATION = 24 * 60 * 60 * 1000; // 24 hours + +/** + * Get storage key for a specific address + */ +function getStorageKey(ethAddress: string): string { + return `${STORAGE_KEY_PREFIX}-${ethAddress}`; +} + +/** + * Get all pending peg-ins from localStorage for an address + */ +export function getPendingPegins(ethAddress: string): PendingPeginRequest[] { + if (!ethAddress) return []; + + try { + const key = getStorageKey(ethAddress); + const stored = localStorage.getItem(key); + if (!stored) return []; + + const parsed: PendingPeginRequest[] = JSON.parse(stored); + return parsed; + } catch (error) { + console.error('Error reading pending peg-ins from localStorage:', error); + return []; + } +} + +/** + * Save pending peg-ins to localStorage + */ +export function savePendingPegins( + ethAddress: string, + pegins: PendingPeginRequest[], +): void { + if (!ethAddress) return; + + try { + const key = getStorageKey(ethAddress); + localStorage.setItem(key, JSON.stringify(pegins)); + } catch (error) { + console.error('Error saving pending peg-ins to localStorage:', error); + } +} + +/** + * Add a new pending peg-in to localStorage + */ +export function addPendingPegin( + ethAddress: string, + pegin: Omit, +): void { + const existingPegins = getPendingPegins(ethAddress); + + const newPegin: PendingPeginRequest = { + ...pegin, + timestamp: Date.now(), + status: 'pending', + }; + + const updatedPegins = [...existingPegins, newPegin]; + savePendingPegins(ethAddress, updatedPegins); +} + +/** + * Remove a pending peg-in from localStorage by ID + */ +export function removePendingPegin(ethAddress: string, peginId: string): void { + const existingPegins = getPendingPegins(ethAddress); + const updatedPegins = existingPegins.filter((p) => p.id !== peginId); + savePendingPegins(ethAddress, updatedPegins); +} + +/** + * Update status of a pending peg-in + */ +export function updatePeginStatus( + ethAddress: string, + peginId: string, + status: PendingPeginRequest['status'], + btcTxHash?: string, +): void { + const existingPegins = getPendingPegins(ethAddress); + const updatedPegins = existingPegins.map((p) => + p.id === peginId + ? { ...p, status, ...(btcTxHash && { btcTxHash }) } + : p, + ); + savePendingPegins(ethAddress, updatedPegins); +} + +/** + * Filter and clean up old pending peg-ins + * Removes peg-ins that have exceeded the max duration + */ +export function filterPendingPegins( + pendingPegins: PendingPeginRequest[], + confirmedPeginIds: string[], +): PendingPeginRequest[] { + const now = Date.now(); + + return pendingPegins.filter((pegin) => { + // Remove if already confirmed + if (confirmedPeginIds.includes(pegin.id)) { + return false; + } + + // Remove if exceeded max duration + const age = now - pegin.timestamp; + if (age > MAX_PENDING_DURATION) { + return false; + } + + return true; + }); +} + +/** + * Clear all pending peg-ins for an address + */ +export function clearPendingPegins(ethAddress: string): void { + if (!ethAddress) return; + + try { + const key = getStorageKey(ethAddress); + localStorage.removeItem(key); + } catch (error) { + console.error('Error clearing pending peg-ins from localStorage:', error); + } +} From 7f4c096905cb41e8d703c0f75e015086f6c17823 Mon Sep 17 00:00:00 2001 From: jeremy-babylonlabs Date: Tue, 7 Oct 2025 21:42:03 +0700 Subject: [PATCH 3/4] feat(packages): resolve comments --- .../vault/src/components/PeginFlow/index.ts | 1 - .../VaultDashboard/VaultActivityCard.tsx | 11 ++- .../VaultDashboard/VaultDashboard.tsx | 89 +++++++++++++++++-- routes/vault/src/hooks/index.ts | 6 +- .../usePeginFlow.ts} | 48 +++++----- routes/vault/src/hooks/useVaultPositions.ts | 26 +++++- 6 files changed, 145 insertions(+), 36 deletions(-) delete mode 100644 routes/vault/src/components/PeginFlow/index.ts rename routes/vault/src/{components/PeginFlow/usePeginFlowState.ts => hooks/usePeginFlow.ts} (61%) diff --git a/routes/vault/src/components/PeginFlow/index.ts b/routes/vault/src/components/PeginFlow/index.ts deleted file mode 100644 index 44dfa4fd..00000000 --- a/routes/vault/src/components/PeginFlow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { usePeginFlowState } from './usePeginFlowState'; diff --git a/routes/vault/src/components/VaultDashboard/VaultActivityCard.tsx b/routes/vault/src/components/VaultDashboard/VaultActivityCard.tsx index 8153afdc..4f6fce5c 100644 --- a/routes/vault/src/components/VaultDashboard/VaultActivityCard.tsx +++ b/routes/vault/src/components/VaultDashboard/VaultActivityCard.tsx @@ -7,12 +7,14 @@ import { ActivityCard, StatusBadge, ProviderItem, + Warning, type ActivityCardData, type ActivityCardDetailItem, } from "@babylonlabs-io/core-ui"; import type { VaultActivity } from "../../mockData/vaultActivities"; import { getVaultState, getActionForState } from "../../utils/vaultState"; import { formatUSDCAmount } from "../../utils/peginTransformers"; +import { bitcoinIcon } from "../../assets"; interface VaultActivityCardProps { activity: VaultActivity; @@ -114,10 +116,17 @@ export function VaultActivityCard({ activity, onBorrow, onRepay }: VaultActivity // Transform to ActivityCardData format const cardData: ActivityCardData = { formattedAmount: `${activity.collateral.amount} ${activity.collateral.symbol}`, - icon: activity.collateral.icon, + icon: activity.collateral.icon || bitcoinIcon, iconAlt: activity.collateral.symbol, details, optionalDetails: optionalDetails.length > 0 ? optionalDetails : undefined, + // Add warning for pending peg-ins + warning: activity.isPending ? ( + + {activity.pendingMessage || + "Your peg-in is being processed. This can take up to ~5 hours while Bitcoin confirmations and provider acknowledgements complete."} + + ) : undefined, primaryAction: getActionForState(vaultState, activity, onBorrow, onRepay), }; diff --git a/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx b/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx index 3a427b02..66650ce8 100644 --- a/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx +++ b/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx @@ -1,15 +1,29 @@ import { ActivityList } from "@babylonlabs-io/core-ui"; +import { useCallback } from "react"; import { BorrowFlow } from "../BorrowFlow"; import { RepayFlow } from "../RepayFlow"; +import { + PeginModal, + PeginSignModal, + PeginSuccessModal, +} from "../modals"; import { useVaultPositions } from "../../hooks/useVaultPositions"; import { useBorrowFlow } from "../../hooks/useBorrowFlow"; import { useRepayFlow } from "../../hooks/useRepayFlow"; +import { usePeginFlow } from "../../hooks/usePeginFlow"; import { EmptyState } from "./EmptyState"; import { VaultActivityCard } from "./VaultActivityCard"; export function VaultDashboard() { // Data fetching - const { activities, isWalletConnected, refetchActivities } = useVaultPositions(); + const { + activities, + isWalletConnected, + refetchActivities, + connectedAddress, + btcAddress, + addPendingPegin, + } = useVaultPositions(); // Borrow flow modal state const { @@ -27,12 +41,47 @@ export function VaultDashboard() { closeRepayFlow, } = useRepayFlow(); - // Handle "New Item" button click - open borrow for first activity - const handleNewBorrow = () => { - if (activities.length > 0) { - openBorrowFlow(activities[0]); + // Peg-in flow modal state + const { + isOpen: peginFlowOpen, + signModalOpen: peginSignModalOpen, + successModalOpen: peginSuccessModalOpen, + peginAmount, + selectedProviders, + btcBalanceSat, + openPeginFlow, + closePeginFlow, + handlePeginClick, + handlePeginSignSuccess: handlePeginSignSuccessBase, + handlePeginSuccessClose, + } = usePeginFlow(); + + // Handle peg-in sign success with storage integration + const handlePeginSignSuccess = useCallback(() => { + // Add to local storage when peg-in is submitted + if (connectedAddress && btcAddress) { + const peginId = `pending-${Date.now()}`; // Temporary ID until we get BTC tx hash + + addPendingPegin({ + id: peginId, + amount: peginAmount.toString(), + providers: selectedProviders, + ethAddress: connectedAddress, + btcAddress: btcAddress, + }); + + console.log('[VaultDashboard] Added pending peg-in to localStorage:', { + id: peginId, + amount: peginAmount, + providers: selectedProviders, + }); } - }; + + // Complete the peg-in flow and refetch activities + handlePeginSignSuccessBase(() => { + refetchActivities(); + }); + }, [connectedAddress, btcAddress, peginAmount, selectedProviders, addPendingPegin, handlePeginSignSuccessBase, refetchActivities]); // Show message if wallet is not connected if (!isWalletConnected) { @@ -42,7 +91,11 @@ export function VaultDashboard() { return ( <>
- + {activities.map((activity) => (
+ {/* Peg-in Modals */} + + + {}} + onSuccess={handlePeginSignSuccess} + amount={peginAmount} + selectedProviders={selectedProviders} + /> + + + ([]); - // Start the peg-in flow - const handleNewBorrow = useCallback(() => { - setPeginModalOpen(true); + const openPeginFlow = useCallback(() => { + setIsOpen(true); + }, []); + + const closePeginFlow = useCallback(() => { + setIsOpen(false); }, []); // Handle peg-in click from PeginModal @@ -23,14 +29,14 @@ export function usePeginFlowState() { console.log("Peg-in clicked:", { amount, providers }); setPeginAmount(amount); setSelectedProviders(providers); - setPeginModalOpen(false); - setPeginSignModalOpen(true); + setIsOpen(false); + setSignModalOpen(true); }, []); // Handle signing success - accepts callback for parent to handle storage const handlePeginSignSuccess = useCallback((onSuccess?: () => void) => { - setPeginSignModalOpen(false); - setPeginSuccessModalOpen(true); + setSignModalOpen(false); + setSuccessModalOpen(true); // Call parent callback if provided if (onSuccess) { @@ -40,27 +46,25 @@ export function usePeginFlowState() { // Handle success modal close const handlePeginSuccessClose = useCallback(() => { - setPeginSuccessModalOpen(false); + setSuccessModalOpen(false); setPeginAmount(0); setSelectedProviders([]); }, []); return { - // Wallet data - btcBalanceSat, // Modal states - peginModalOpen, - peginSignModalOpen, - peginSuccessModalOpen, + isOpen, + signModalOpen, + successModalOpen, // Peg-in data peginAmount, selectedProviders, + btcBalanceSat, // Actions - handleNewBorrow, + openPeginFlow, + closePeginFlow, handlePeginClick, handlePeginSignSuccess, handlePeginSuccessClose, - setPeginModalOpen, - setPeginSignModalOpen, }; -} \ No newline at end of file +} diff --git a/routes/vault/src/hooks/useVaultPositions.ts b/routes/vault/src/hooks/useVaultPositions.ts index 93a0eac5..d0016054 100644 --- a/routes/vault/src/hooks/useVaultPositions.ts +++ b/routes/vault/src/hooks/useVaultPositions.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useChainConnector } from "@babylonlabs-io/wallet-connector"; import type { Hex } from "viem"; -import { usePeginRequests } from "./usePeginRequests"; +import { usePeginRequests, usePeginStorage } from "./usePeginRequests"; /** * Hook to manage vault positions data fetching and wallet connection @@ -9,6 +9,13 @@ import { usePeginRequests } from "./usePeginRequests"; */ export function useVaultPositions() { const ethConnector = useChainConnector('ETH'); + const btcConnector = useChainConnector('BTC'); + + // Get BTC address from connector + const btcAddress = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (btcConnector as any)?.connectedWallet?.account?.address as string | undefined; + }, [btcConnector]); const connectedAddress = useMemo(() => { const address = ( @@ -21,15 +28,26 @@ export function useVaultPositions() { }, [ethConnector]); // Fetch pegin requests from blockchain - // Note: We pass a no-op function since we don't need borrow callback here anymore - const { activities, refetch } = usePeginRequests( + const { activities: confirmedActivities, refetch } = usePeginRequests( connectedAddress, () => {} // no-op callback ); + // Integrate local storage for pending peg-ins + const { + allActivities, + addPendingPegin, + } = usePeginStorage({ + ethAddress: connectedAddress || '', + confirmedPegins: confirmedActivities, + }); + return { - activities, + activities: allActivities, isWalletConnected: !!connectedAddress, refetchActivities: refetch, + connectedAddress, + btcAddress, + addPendingPegin, }; } From 3629c3a1df8908fa0d9b65c5dd11b64b6741e7f2 Mon Sep 17 00:00:00 2001 From: jeremy-babylonlabs Date: Tue, 7 Oct 2025 21:42:03 +0700 Subject: [PATCH 4/4] feat(packages): resolve comments --- routes/vault/src/api/index.ts | 7 ------- .../vault-providers-api/index.ts} | 2 +- .../vault/src/components/VaultDashboard/VaultDashboard.tsx | 3 ++- routes/vault/src/components/modals/PeginModal.tsx | 2 +- routes/vault/src/hooks/useVaultPositions.ts | 3 ++- routes/vault/src/hooks/useVaultProviders.ts | 2 +- 6 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 routes/vault/src/api/index.ts rename routes/vault/src/{api/getVaultProviders.ts => clients/vault-providers-api/index.ts} (98%) diff --git a/routes/vault/src/api/index.ts b/routes/vault/src/api/index.ts deleted file mode 100644 index f38c1f63..00000000 --- a/routes/vault/src/api/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * API Module - * - * Centralized exports for all API functions - */ - -export * from './getVaultProviders'; \ No newline at end of file diff --git a/routes/vault/src/api/getVaultProviders.ts b/routes/vault/src/clients/vault-providers-api/index.ts similarity index 98% rename from routes/vault/src/api/getVaultProviders.ts rename to routes/vault/src/clients/vault-providers-api/index.ts index dde3c9db..67ccc978 100644 --- a/routes/vault/src/api/getVaultProviders.ts +++ b/routes/vault/src/clients/vault-providers-api/index.ts @@ -1,5 +1,5 @@ /** - * Vault Provider API + * Vault Provider API Client * * This module handles fetching vault provider data. * Currently returns mock data, but structured to support GraphQL integration. diff --git a/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx b/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx index 66650ce8..38f6d9ad 100644 --- a/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx +++ b/routes/vault/src/components/VaultDashboard/VaultDashboard.tsx @@ -13,6 +13,7 @@ import { useRepayFlow } from "../../hooks/useRepayFlow"; import { usePeginFlow } from "../../hooks/usePeginFlow"; import { EmptyState } from "./EmptyState"; import { VaultActivityCard } from "./VaultActivityCard"; +import type { VaultActivity } from "../../mockData/vaultActivities"; export function VaultDashboard() { // Data fetching @@ -96,7 +97,7 @@ export function VaultDashboard() { isEmpty={activities.length === 0} isConnected={isWalletConnected} > - {activities.map((activity) => ( + {activities.map((activity: VaultActivity) => (