diff --git a/paystell-frontend/src/app/api/deposit/[id]/route.ts b/paystell-frontend/src/app/api/deposit/[id]/route.ts new file mode 100644 index 0000000..9667842 --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/[id]/route.ts @@ -0,0 +1,158 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositRequest } from "@/lib/types/deposit"; + +import { depositStore } from "../deposit-store"; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + + // 2. Get deposit request + const deposit = depositStore.get(id); + if (!deposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (deposit.ownerId !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + return NextResponse.json({ + success: true, + deposit, + }); + } catch (error: unknown) { + console.error("Deposit retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + const updates = await request.json(); + + // 2. Get existing deposit request + const existingDeposit = depositStore.get(id); + if (!existingDeposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (existingDeposit.ownerId !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + // 4. Validate updates + const allowedUpdates = ["status", "transactionHash", "confirmedAt"]; + const validUpdates: Partial = {}; + + for (const [key, value] of Object.entries(updates)) { + if (allowedUpdates.includes(key)) { + (validUpdates as Record)[key] = value; + } + } + + // 5. Update deposit request + const updatedDeposit = depositStore.update(id, validUpdates); + + return NextResponse.json({ + success: true, + deposit: updatedDeposit, + }); + } catch (error: unknown) { + console.error("Deposit update error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to update deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + + // 2. Get existing deposit request + const existingDeposit = depositStore.get(id); + if (!existingDeposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (existingDeposit.ownerId !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + // 4. Delete deposit request + depositStore.delete(id); + + return NextResponse.json({ + success: true, + message: "Deposit request deleted", + }); + } catch (error: unknown) { + console.error("Deposit deletion error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to delete deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/api/deposit/deposit-store.ts b/paystell-frontend/src/app/api/deposit/deposit-store.ts new file mode 100644 index 0000000..44b71b3 --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/deposit-store.ts @@ -0,0 +1,39 @@ +import { DepositRequest } from "@/lib/types/deposit"; + +// Shared in-memory store for deposit requests +// In production, use a database +export const depositRequests = new Map(); + +// Helper functions for deposit management +export const depositStore = { + create: (id: string, deposit: DepositRequest) => { + depositRequests.set(id, deposit); + return deposit; + }, + + get: (id: string) => { + return depositRequests.get(id); + }, + + update: (id: string, updates: Partial) => { + const existing = depositRequests.get(id); + if (!existing) return null; + + const updated = { ...existing, ...updates }; + depositRequests.set(id, updated); + return updated; + }, + + delete: (id: string) => { + return depositRequests.delete(id); + }, + + getAll: () => { + return Array.from(depositRequests.values()); + }, + + getByUser: (userId: string) => { + return Array.from(depositRequests.values()) + .filter(deposit => deposit.ownerId === userId); + } +}; diff --git a/paystell-frontend/src/app/api/deposit/monitor/route.ts b/paystell-frontend/src/app/api/deposit/monitor/route.ts new file mode 100644 index 0000000..0224d6c --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/monitor/route.ts @@ -0,0 +1,200 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositMonitoringConfig } from "@/lib/types/deposit"; +import { isValidStellarAddress } from "@/lib/deposit/deposit-utils"; + +// In-memory store for monitoring configurations per user +// In production, use a database +const monitoringConfigs = new Map>(); + +export async function POST(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { address, asset, minAmount, maxAmount, memo } = await request.json(); + + // 2. Input validation + if (!address || !asset) { + return NextResponse.json( + { message: "Address and asset are required" }, + { status: 400 } + ); + } + + // 3. Validate Stellar address + if (!isValidStellarAddress(address)) { + return NextResponse.json( + { message: "Invalid Stellar address" }, + { status: 400 } + ); + } + + // 4. Validate asset + const supportedAssets = ["XLM", "USDC", "USDT"]; + if (!supportedAssets.includes(asset)) { + return NextResponse.json( + { message: "Unsupported asset" }, + { status: 400 } + ); + } + + // 5. Validate amounts if provided + if (minAmount && (isNaN(parseFloat(minAmount)) || parseFloat(minAmount) <= 0)) { + return NextResponse.json( + { message: "Invalid minimum amount" }, + { status: 400 } + ); + } + + if (maxAmount && (isNaN(parseFloat(maxAmount)) || parseFloat(maxAmount) <= 0)) { + return NextResponse.json( + { message: "Invalid maximum amount" }, + { status: 400 } + ); + } + + if (minAmount && maxAmount && parseFloat(minAmount) > parseFloat(maxAmount)) { + return NextResponse.json( + { message: "Minimum amount cannot be greater than maximum amount" }, + { status: 400 } + ); + } + + // 6. Create monitoring configuration + const config: DepositMonitoringConfig = { + address, + asset, + minAmount: minAmount || undefined, + maxAmount: maxAmount || undefined, + memo: memo || undefined, + }; + + // 7. Store monitoring configuration per user + const userId = session.user.id; + const key = `${address}_${asset}`; + + if (!monitoringConfigs.has(userId)) { + monitoringConfigs.set(userId, new Map()); + } + + monitoringConfigs.get(userId)!.set(key, config); + + return NextResponse.json({ + success: true, + config, + message: "Monitoring started", + }); + } catch (error: unknown) { + console.error("Monitoring setup error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to setup monitoring"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function GET(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const asset = searchParams.get("asset"); + + // 2. Get monitoring configurations for this user only + const userId = session.user.id; + const userConfigs = monitoringConfigs.get(userId) || new Map(); + let configs = Array.from(userConfigs.values()); + + // 3. Filter by address if provided + if (address) { + configs = configs.filter(config => config.address === address); + } + + // 4. Filter by asset if provided + if (asset) { + configs = configs.filter(config => config.asset === asset); + } + + return NextResponse.json({ + success: true, + configs, + total: configs.length, + }); + } catch (error: unknown) { + console.error("Monitoring retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve monitoring configs"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const asset = searchParams.get("asset"); + + // 2. Input validation + if (!address || !asset) { + return NextResponse.json( + { message: "Address and asset are required" }, + { status: 400 } + ); + } + + // 3. Remove monitoring configuration for this user only + const userId = session.user.id; + const key = `${address}_${asset}`; + + const userConfigs = monitoringConfigs.get(userId); + if (!userConfigs) { + return NextResponse.json( + { message: "Monitoring configuration not found" }, + { status: 404 } + ); + } + + const deleted = userConfigs.delete(key); + + if (!deleted) { + return NextResponse.json( + { message: "Monitoring configuration not found" }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: "Monitoring stopped", + }); + } catch (error: unknown) { + console.error("Monitoring removal error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to remove monitoring"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/api/deposit/route.ts b/paystell-frontend/src/app/api/deposit/route.ts new file mode 100644 index 0000000..10d7565 --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/route.ts @@ -0,0 +1,145 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositRequest } from "@/lib/types/deposit"; +import { generateDepositId, calculateDepositExpiration, isValidStellarAddress } from "@/lib/deposit/deposit-utils"; +import { paymentRateLimit } from "@/middleware/rateLimit"; + +import { depositStore } from "./deposit-store"; + +export async function POST(request: NextRequest) { + try { + // Apply rate limiting + const rateLimitResponse = paymentRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { amount, asset, memo, customAddress } = await request.json(); + + // 2. Input validation + if (!asset) { + return NextResponse.json( + { message: "Asset is required" }, + { status: 400 } + ); + } + + // 3. Validate asset + const supportedAssets = ["XLM", "USDC", "USDT"]; + if (!supportedAssets.includes(asset)) { + return NextResponse.json( + { message: "Unsupported asset" }, + { status: 400 } + ); + } + + // 4. Validate amount if provided + if (amount && (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0)) { + return NextResponse.json( + { message: "Invalid amount" }, + { status: 400 } + ); + } + + // 5. Validate custom address if provided + if (customAddress && !isValidStellarAddress(customAddress)) { + return NextResponse.json( + { message: "Invalid Stellar address" }, + { status: 400 } + ); + } + + // 6. Validate custom address is provided + if (!customAddress) { + return NextResponse.json( + { message: "Deposit address is required" }, + { status: 400 } + ); + } + + // 7. Create deposit request + const depositRequest: DepositRequest = { + id: generateDepositId(), + ownerId: session.user.id, + address: customAddress, + amount: amount || undefined, + asset, + memo: memo || undefined, + status: "pending", + createdAt: new Date().toISOString(), + expiresAt: calculateDepositExpiration(), + }; + + // 8. Store deposit request + depositStore.create(depositRequest.id, depositRequest); + + return NextResponse.json({ + success: true, + deposit: depositRequest, + }); + } catch (error: unknown) { + console.error("Deposit creation error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to create deposit request"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function GET(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const requestedUserId = searchParams.get("userId"); + const status = searchParams.get("status"); + + // 2. Security check - prevent access to other users' deposits + if (requestedUserId && requestedUserId !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + const targetAddress = requestedUserId ?? session.user.id; + + // 3. Get deposits for user + let userDeposits = depositStore.getByUser(targetAddress); + + // 4. Filter by status if provided + if (status) { + userDeposits = userDeposits.filter(deposit => deposit.status === status); + } + + // 5. Sort by creation date (newest first) + userDeposits.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + return NextResponse.json({ + success: true, + deposits: userDeposits, + total: userDeposits.length, + }); + } catch (error: unknown) { + console.error("Deposit retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve deposits"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/dashboard/deposit/page.tsx b/paystell-frontend/src/app/dashboard/deposit/page.tsx new file mode 100644 index 0000000..6958fda --- /dev/null +++ b/paystell-frontend/src/app/dashboard/deposit/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { DepositFlow } from "@/components/deposit/DepositFlow"; +import { OptimisticTransactionList } from "@/components/optimistic/OptimisticTransactionList"; +import { TransactionMonitor } from "@/components/monitoring/TransactionMonitor"; +import { TransactionQueueManager } from "@/components/queue/TransactionQueueManager"; +import { WebSocketStatus } from "@/components/websocket/WebSocketStatus"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; + +export default function DepositPage() { + const { isConnected, publicKey, connectWallet, connecting, error } = useWalletStore(); + const [activeTab, setActiveTab] = useState("deposit"); + + // Initialize monitoring for connected wallet + useEffect(() => { + if (isConnected && publicKey) { + // Start monitoring for XLM deposits + // This would typically be done through the monitoring hook + console.log(`Monitoring deposits for wallet: ${publicKey}`); + } + }, [isConnected, publicKey]); + + if (!isConnected) { + return ( +
+
+

Deposit Funds

+

+ Please connect your Stellar wallet to access deposit features +

+ + {error && ( +

{error}

+ )} +
+
+ ); + } + + return ( +
+
+

Deposit & Transaction Management

+

+ Manage your deposits, monitor transactions, and track optimistic updates +

+
+ + + + + Deposit + 💰 + + + Optimistic + + + + Monitor + 👁️ + + + Queue + 📋 + + + WebSocket + 🔌 + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/paystell-frontend/src/app/layout.tsx b/paystell-frontend/src/app/layout.tsx index 3ca58a9..eb82437 100644 --- a/paystell-frontend/src/app/layout.tsx +++ b/paystell-frontend/src/app/layout.tsx @@ -1,18 +1,18 @@ -import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; -import './globals.css'; -import { AuthProvider } from '@/providers/AuthProvider'; -import { ThemeProvider } from 'next-themes'; -import { WalletProvider } from '@/providers/useWalletProvider'; -import { Toaster } from '@/components/ui/sonner'; -import { ErrorBoundary } from '@/components/ErrorBoundary'; -import { OfflineBanner, OnlineBanner } from '@/components/OfflineBanner'; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { AuthProvider } from "@/providers/AuthProvider"; +import { ThemeProvider } from "next-themes"; +import { WalletProvider } from "@/providers/useWalletProvider"; +import { Toaster } from "@/components/ui/sonner"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { OfflineBanner, OnlineBanner } from "@/components/OfflineBanner"; -const inter = Inter({ subsets: ['latin'] }); +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: 'PayStell', - description: 'Payment Platform', + title: "PayStell", + description: "Payment Platform", }; export default function RootLayout({ @@ -39,3 +39,4 @@ export default function RootLayout({ ); } + diff --git a/paystell-frontend/src/components/dashboard/nav/index.tsx b/paystell-frontend/src/components/dashboard/nav/index.tsx index d660da9..a46d4da 100644 --- a/paystell-frontend/src/components/dashboard/nav/index.tsx +++ b/paystell-frontend/src/components/dashboard/nav/index.tsx @@ -1,15 +1,15 @@ 'use client'; -import { cn } from '@/lib/utils'; -import type { NavProps } from './types'; -import { MobileTrigger } from './mobile-trigger'; -import { NavItem } from './nav-item'; -import { navStyles } from './styles'; -import { Logo } from '@/components/dashboard/nav/Logo'; -import { useAuth } from '@/providers/AuthProvider'; -import { useRouter } from 'next/navigation'; -import { IoLogOutOutline } from 'react-icons/io5'; -import { useEffect, useCallback } from 'react'; +import { cn } from "@/lib/utils"; +import type { NavProps } from "./types"; +import { MobileTrigger } from "./mobile-trigger"; +import { NavItem } from "./nav-item"; +import { navStyles } from "./styles"; +import { Logo } from "@/components/dashboard/nav/Logo"; +import { useAuth } from "@/providers/AuthProvider"; +import { useRouter } from "next/navigation"; +import { IoLogOutOutline } from "react-icons/io5"; +import { useEffect, useCallback } from "react"; export function Nav({ items, diff --git a/paystell-frontend/src/components/deposit/DepositFlow.tsx b/paystell-frontend/src/components/deposit/DepositFlow.tsx new file mode 100644 index 0000000..0fa74cd --- /dev/null +++ b/paystell-frontend/src/components/deposit/DepositFlow.tsx @@ -0,0 +1,258 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ArrowLeft, Download, Clock, QrCode } from "lucide-react"; +import { DepositForm } from "./DepositForm"; +import { DepositQRCode } from "./DepositQRCode"; +import { DepositHistory } from "./DepositHistory"; +import { DepositRequest, DepositTransaction } from "@/lib/types/deposit"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; +import { toast } from "sonner"; + +interface DepositFlowProps { + className?: string; +} + +export function DepositFlow({ className }: DepositFlowProps) { + const { isConnected, publicKey } = useWalletStore(); + const [deposits, setDeposits] = useState([]); + const [transactions, setTransactions] = useState([]); + const [activeDeposit, setActiveDeposit] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Load deposits and transactions from localStorage on mount + useEffect(() => { + loadDeposits(); + loadTransactions(); + }, []); + + const loadDeposits = () => { + try { + const stored = localStorage.getItem("paystell_deposits"); + if (stored) { + setDeposits(JSON.parse(stored)); + } + } catch (error) { + console.error("Error loading deposits:", error); + } + }; + + const loadTransactions = () => { + try { + const stored = localStorage.getItem("paystell_deposit_transactions"); + if (stored) { + setTransactions(JSON.parse(stored)); + } + } catch (error) { + console.error("Error loading transactions:", error); + } + }; + + const saveDeposits = (newDeposits: DepositRequest[]) => { + try { + localStorage.setItem("paystell_deposits", JSON.stringify(newDeposits)); + setDeposits(newDeposits); + } catch (error) { + console.error("Error saving deposits:", error); + } + }; + + const saveTransactions = (newTransactions: DepositTransaction[]) => { + try { + localStorage.setItem("paystell_deposit_transactions", JSON.stringify(newTransactions)); + setTransactions(newTransactions); + } catch (error) { + console.error("Error saving transactions:", error); + } + }; + + const handleCreateDeposit = (deposit: DepositRequest) => { + const newDeposits = [...deposits, deposit]; + saveDeposits(newDeposits); + setActiveDeposit(deposit); + toast.success("Deposit request created successfully"); + }; + + const handleViewDeposit = (deposit: DepositRequest) => { + setActiveDeposit(deposit); + }; + + const handleRefresh = async () => { + setIsLoading(true); + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + loadDeposits(); + loadTransactions(); + toast.success("Data refreshed"); + } catch (error) { + toast.error("Failed to refresh data"); + } finally { + setIsLoading(false); + } + }; + + const handleBackToForm = () => { + setActiveDeposit(null); + }; + + if (!isConnected) { + return ( + + +
+ +
+

Connect Your Wallet

+

+ Please connect your Stellar wallet to create deposit requests +

+
+
+
+
+ ); + } + + if (activeDeposit) { + return ( +
+
+ +
+

Deposit Request

+

+ ID: {activeDeposit.id} +

+
+
+ +
+ + + + + Deposit Instructions + + +
+

How to deposit:

+
    +
  1. Scan the QR code with your mobile wallet
  2. +
  3. Or copy the address and send funds manually
  4. +
  5. Wait for transaction confirmation
  6. +
  7. Your balance will update automatically
  8. +
+
+ +
+

Supported Assets:

+
    +
  • Stellar Lumens (XLM)
  • +
  • USD Coin (USDC)
  • +
  • Tether (USDT)
  • +
+
+ +
+

Important Notes:

+
    +
  • Only send supported assets to this address
  • +
  • Minimum deposit: 1 XLM
  • +
  • Deposit requests expire in 24 hours
  • +
  • Transactions may take a few minutes to confirm
  • +
+
+
+
+
+
+ ); + } + + return ( +
+
+

Deposit Funds

+

+ Create deposit requests and receive funds to your Stellar wallet +

+
+ + + + + + Create Deposit + + + + History + + + + +
+ + + + + Quick Deposit + + +
+

Your Wallet Address

+
+ + {publicKey} + +
+
+ +
+

Supported Assets

+
+ XLM + USDC + USDT +
+
+ +
+

Network

+

+ Stellar Testnet +

+
+
+
+
+
+ + + + +
+
+ ); +} diff --git a/paystell-frontend/src/components/deposit/DepositForm.tsx b/paystell-frontend/src/components/deposit/DepositForm.tsx new file mode 100644 index 0000000..77d80cd --- /dev/null +++ b/paystell-frontend/src/components/deposit/DepositForm.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Download, QrCode } from "lucide-react"; +import { DepositRequest } from "@/lib/types/deposit"; +import { generateDepositId, calculateDepositExpiration, isValidStellarAddress } from "@/lib/deposit/deposit-utils"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; +import { toast } from "sonner"; + +interface DepositFormProps { + onCreateDeposit: (deposit: DepositRequest) => void; + onCancel?: () => void; + className?: string; +} + +const SUPPORTED_ASSETS = [ + { value: "XLM", label: "Stellar Lumens (XLM)" }, + { value: "USDC", label: "USD Coin (USDC)" }, + { value: "USDT", label: "Tether (USDT)" }, +]; + +export function DepositForm({ onCreateDeposit, onCancel, className }: DepositFormProps) { + const { publicKey, isConnected } = useWalletStore(); + const [formData, setFormData] = useState({ + amount: "", + asset: "XLM", + memo: "", + customAddress: "", + useCustomAddress: false, + }); + const [isCreating, setIsCreating] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.useCustomAddress && (!isConnected || !publicKey)) { + toast.error("Please connect your wallet first"); + return; + } + + setIsCreating(true); + + try { + const depositAddress = formData.useCustomAddress + ? formData.customAddress + : publicKey; + + if (!depositAddress || !isValidStellarAddress(depositAddress)) { + toast.error("Invalid Stellar address"); + return; + } + + const deposit: DepositRequest = { + id: generateDepositId(), + ownerId: "mock-user", // This will be replaced by the API + address: depositAddress, + amount: formData.amount || undefined, + asset: formData.asset, + memo: formData.memo || undefined, + status: "pending", + createdAt: new Date().toISOString(), + expiresAt: calculateDepositExpiration(), + }; + + onCreateDeposit(deposit); + + // Reset form + setFormData({ + amount: "", + asset: "XLM", + memo: "", + customAddress: "", + useCustomAddress: false, + }); + } catch (error) { + console.error("Error creating deposit:", error); + toast.error("Failed to create deposit request"); + } finally { + setIsCreating(false); + } + }; + + const handleInputChange = (field: string, value: string | boolean) => { + setFormData(prev => ({ + ...prev, + [field]: value, + })); + }; + + return ( + + + + + Create Deposit Request + + + +
+ {/* Asset Selection */} +
+ + +
+ + {/* Amount Input */} +
+ + handleInputChange("amount", e.target.value)} + step="0.0000001" + min="0" + /> +

+ Leave empty to allow any amount +

+
+ + {/* Memo Input */} +
+ +