From f83054b208d6152b0bad74501fc4af0347db03c5 Mon Sep 17 00:00:00 2001 From: Rassl Date: Fri, 12 Jun 2026 15:37:52 +0000 Subject: [PATCH] Generated with Hive: Add withdraw sats flow to wallet modal with invoice validation and history --- src/components/modals/budget-modal.tsx | 163 +++++++++++++++++++- src/lib/__tests__/budget-modal.test.tsx | 191 +++++++++++++++++++++++- src/lib/__tests__/invoice-utils.test.ts | 20 ++- src/lib/__tests__/withdraw.test.ts | 92 ++++++++++++ src/lib/invoice-utils.ts | 16 ++ src/lib/sphinx/index.ts | 2 +- src/lib/sphinx/payment.ts | 20 +++ src/lib/transaction-display.ts | 2 + 8 files changed, 496 insertions(+), 10 deletions(-) create mode 100644 src/lib/__tests__/withdraw.test.ts diff --git a/src/components/modals/budget-modal.tsx b/src/components/modals/budget-modal.tsx index fa72abc..ead246f 100644 --- a/src/components/modals/budget-modal.tsx +++ b/src/components/modals/budget-modal.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import { Zap, Copy, Check, Loader2, ArrowLeft, History, Key, RefreshCw, ArrowUpRight, Clock } from "lucide-react" +import { Zap, Copy, Check, Loader2, ArrowLeft, History, Key, RefreshCw, ArrowUpRight, Clock, ArrowDownLeft } from "lucide-react" import { QRCodeSVG } from "qrcode.react" import { Dialog, @@ -13,18 +13,85 @@ import { import { Button } from "@/components/ui/button" import { useModalStore } from "@/stores/modal-store" import { useUserStore } from "@/stores/user-store" -import { isSphinx, hasWebLN, payInvoice, payL402, topUpLsat, fetchTransactionHistory, pollPaymentStatus, fetchBuyLsatChallenge, savePendingLsat, getPendingLsat, clearPendingLsat, topUpStatus, TransactionRow, PendingLsatChallenge } from "@/lib/sphinx" +import { isSphinx, hasWebLN, payInvoice, payL402, topUpLsat, fetchTransactionHistory, pollPaymentStatus, fetchBuyLsatChallenge, savePendingLsat, getPendingLsat, clearPendingLsat, topUpStatus, withdraw, TransactionRow, PendingLsatChallenge } from "@/lib/sphinx" import { getActionDisplayLabel, getActionBadgeColor, isViewGrantRow } from "@/lib/transaction-display" import { isMocksEnabled, MOCK_TRANSACTIONS } from "@/lib/mock-data" import { cookieStorage } from "@/lib/cookie-storage" import { api } from "@/lib/api" -import { decodeInvoiceExpiry } from "@/lib/invoice-utils" +import { decodeInvoiceExpiry, decodeInvoiceAmountSats } from "@/lib/invoice-utils" import { formatCountdown } from "@/lib/format-countdown" import { useInvoiceCountdown } from "@/hooks/use-invoice-countdown" -type Step = "balance" | "first-purchase" | "first-invoice" | "amount" | "invoice" | "success" | "history" | "manage-token" | "restore" +type Step = "balance" | "first-purchase" | "first-invoice" | "amount" | "invoice" | "success" | "history" | "manage-token" | "restore" | "withdraw" const PRESET_AMOUNTS = [50, 100, 500, 1000] +const MINIMUM_WITHDRAWAL_SATS = 100 + +function WithdrawStep({ + invoice, + onInvoiceChange, + error, + loading, + onConfirm, +}: { + invoice: string + onInvoiceChange: (val: string) => void + error: string + loading: boolean + onConfirm: () => void +}) { + const decodedAmountSats = invoice.trim() ? decodeInvoiceAmountSats(invoice) : null + const withdrawExpiresAt = invoice.trim() ? decodeInvoiceExpiry(invoice) : null + const { secondsLeft, expired } = useInvoiceCountdown(withdrawExpiresAt) + + return ( + <> +