Skip to content

Commit aa049d1

Browse files
committed
wallet stuff
1 parent e3da603 commit aa049d1

File tree

10 files changed

+252
-16
lines changed

10 files changed

+252
-16
lines changed

src/index.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ html {
2525
@apply rounded-full;
2626
}
2727

28+
.btn:disabled {
29+
opacity: 0.3;
30+
}
31+
32+
/* Exception for buttons with animate-spin - keep more visible */
33+
.btn:disabled:has(.animate-spin) {
34+
opacity: 0.7;
35+
}
36+
2837
/* Default theme */
2938
.btn-primary {
3039
@apply text-base-content;

src/pages/wallet/CashuWallet.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ReceiveDialog from "./cashu/ReceiveDialog"
1616
import QRScannerModal from "./cashu/QRScannerModal"
1717
import HistoryList from "./cashu/HistoryList"
1818
import MintsList from "./cashu/MintsList"
19+
import TransactionDetailsModal from "./cashu/TransactionDetailsModal"
1920
import {formatUsd} from "./cashu/utils"
2021
import {Link, useNavigate, useLocation} from "@/navigation"
2122
import Header from "@/shared/components/header/Header"
@@ -79,6 +80,10 @@ export default function CashuWallet() {
7980
const [receiveDialogInitialToken, setReceiveDialogInitialToken] = useState<string>("")
8081
const [receiveDialogInitialInvoice, setReceiveDialogInitialInvoice] = useState<string>("")
8182
const [refreshing, setRefreshing] = useState(false)
83+
const [showTransactionDetails, setShowTransactionDetails] = useState(false)
84+
const [selectedTransaction, setSelectedTransaction] = useState<EnrichedHistoryEntry | null>(
85+
null
86+
)
8287
const [qrError, setQrError] = useState<string>("")
8388
const [isOffline, setIsOffline] = useState(!navigator.onLine)
8489
const [showToS, setShowToS] = useState(false)
@@ -208,6 +213,11 @@ export default function CashuWallet() {
208213
}
209214
}, [])
210215

216+
const handleReceiveEntryClick = useCallback((entry: HistoryEntry) => {
217+
setSelectedTransaction(entry as EnrichedHistoryEntry)
218+
setShowTransactionDetails(true)
219+
}, [])
220+
211221
const handleCloseSendDialog = () => {
212222
setShowSendDialog(false)
213223
setSendDialogInitialToken(undefined)
@@ -224,7 +234,34 @@ export default function CashuWallet() {
224234
console.log("🔄 Manual refresh button clicked")
225235
setRefreshing(true)
226236
try {
227-
// Check pending melt quotes (for stuck Lightning payments)
237+
// Check and redeem pending mint quotes (for stuck incoming Lightning payments)
238+
if (manager) {
239+
console.log("🔍 Checking and requeueing paid mint quotes")
240+
try {
241+
const result = await manager.quotes.requeuePaidMintQuotes()
242+
console.log(`✅ Requeued ${result.requeued.length} paid mint quotes for redemption`)
243+
if (result.requeued.length > 0) {
244+
console.log("⏳ Waiting for quotes to be processed...")
245+
// Give processor time to redeem quotes
246+
await new Promise((resolve) => setTimeout(resolve, 3000))
247+
}
248+
} catch (err) {
249+
console.error("Failed to requeue mint quotes:", err)
250+
}
251+
252+
// Force recalculate balance from all proofs in database
253+
// This catches any old redeemed quotes that weren't reflected in balance
254+
console.log("🔍 Recalculating balance from all proofs")
255+
try {
256+
const freshBalance = await manager.wallet.getBalances()
257+
console.log("💰 Fresh balance:", freshBalance)
258+
setBalance(freshBalance)
259+
} catch (err) {
260+
console.error("Failed to recalculate balance:", err)
261+
}
262+
}
263+
264+
// Check pending melt quotes (for stuck outgoing Lightning payments)
228265
if (manager && balance) {
229266
const mints = Object.keys(balance)
230267
console.log("🔍 Checking pending melt quotes on mints:", mints)
@@ -464,6 +501,7 @@ export default function CashuWallet() {
464501
mgr.on("receive:created", () => updateData("receive:created")),
465502
mgr.on("mint-quote:created", () => updateData("mint-quote:created")),
466503
mgr.on("mint-quote:redeemed", () => updateData("mint-quote:redeemed")),
504+
mgr.on("proofs:saved", () => updateData("proofs:saved")),
467505
]
468506

469507
return () => {
@@ -571,7 +609,7 @@ export default function CashuWallet() {
571609
<button
572610
onClick={handleRefresh}
573611
disabled={refreshing}
574-
className="btn btn-circle btn-ghost btn-sm flex-shrink-0"
612+
className="btn btn-circle btn-ghost btn-sm flex-shrink-0 disabled:opacity-70"
575613
title="Refresh"
576614
>
577615
<RiRefreshLine className={`w-5 h-5 ${refreshing ? "animate-spin" : ""}`} />
@@ -704,6 +742,7 @@ export default function CashuWallet() {
704742
usdRate={usdRate}
705743
onSendEntryClick={handleSendEntryClick}
706744
onMintEntryClick={handleMintEntryClick}
745+
onReceiveEntryClick={handleReceiveEntryClick}
707746
/>
708747
</div>
709748
)}
@@ -756,6 +795,16 @@ export default function CashuWallet() {
756795
onScanSuccess={handleQRScanSuccess}
757796
/>
758797

798+
<TransactionDetailsModal
799+
isOpen={showTransactionDetails}
800+
onClose={() => {
801+
setShowTransactionDetails(false)
802+
setSelectedTransaction(null)
803+
}}
804+
entry={selectedTransaction}
805+
usdRate={usdRate}
806+
/>
807+
759808
{qrError && (
760809
<div className="fixed bottom-24 left-1/2 -translate-x-1/2 z-50 max-w-md">
761810
<div className="alert alert-error">

src/pages/wallet/cashu/HistoryList.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ interface HistoryListProps {
1717
usdRate: number | null
1818
onSendEntryClick?: (entry: SendHistoryEntry) => void
1919
onMintEntryClick?: (entry: HistoryEntry) => void
20+
onReceiveEntryClick?: (entry: HistoryEntry) => void
2021
}
2122

2223
export default function HistoryList({
2324
history,
2425
usdRate,
2526
onSendEntryClick,
2627
onMintEntryClick,
28+
onReceiveEntryClick,
2729
}: HistoryListProps) {
2830
const [displayCount, setDisplayCount] = useState(INITIAL_DISPLAY)
2931
const navigate = useNavigate()
@@ -46,13 +48,15 @@ export default function HistoryList({
4648
const amount = getTransactionAmount(entry)
4749
const status = getTransactionStatus(entry)
4850
const isSend = entry.type === "send"
51+
const isReceive = entry.type === "receive" || (entry.type === "mint" && !status)
4952
const isPendingMint = entry.type === "mint" && status === "pending"
5053
const isZapWithEvent =
5154
entry.paymentMetadata?.type === "zap" && entry.paymentMetadata?.eventId
5255
const hasRecipient = !!entry.paymentMetadata?.recipient
5356
const hasSender = !!entry.paymentMetadata?.sender
5457
const isClickable =
5558
(isSend && onSendEntryClick) ||
59+
(isReceive && onReceiveEntryClick) ||
5660
(isPendingMint && onMintEntryClick) ||
5761
isZapWithEvent ||
5862
hasRecipient ||
@@ -65,6 +69,9 @@ export default function HistoryList({
6569
const handleClick = () => {
6670
if (isPendingMint && onMintEntryClick) {
6771
onMintEntryClick(entry)
72+
} else if (isReceive && onReceiveEntryClick && !hasRecipient && !hasSender && !isZapWithEvent) {
73+
// Settled receives without recipient/sender metadata - show details
74+
onReceiveEntryClick(entry)
6875
} else if (isZapWithEvent && entry.paymentMetadata?.eventId) {
6976
navigate(`/${nip19.noteEncode(entry.paymentMetadata.eventId)}`)
7077
} else if (hasRecipient && entry.paymentMetadata?.recipient) {

src/pages/wallet/cashu/MintDetailsModal.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface MintDetailsModalProps {
2424
onMintDeleted: () => void
2525
activeMint: string | null
2626
onSetActive: (mintUrl: string) => void
27+
balance?: number
2728
}
2829

2930
export default function MintDetailsModal({
@@ -34,6 +35,7 @@ export default function MintDetailsModal({
3435
onMintDeleted,
3536
activeMint,
3637
onSetActive,
38+
balance,
3739
}: MintDetailsModalProps) {
3840
const {getCachedMintInfo, setCachedMintInfo, clearMintInfoCache} = useCashuWalletStore()
3941
const [mintInfo, setMintInfo] = useState<MintInfo | null>(null)
@@ -180,6 +182,9 @@ export default function MintDetailsModal({
180182
<h2 className="text-2xl font-bold mb-2">
181183
{mintInfo?.name || "Unknown Mint"}
182184
</h2>
185+
{balance !== undefined && (
186+
<div className="text-xl text-base-content/70 mb-4">{balance} bit</div>
187+
)}
183188
{qrCodeUrl && (
184189
<div className="flex justify-center my-4">
185190
<div className="bg-white rounded-lg p-4">

src/pages/wallet/cashu/MintsList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export default function MintsList({
161161
}}
162162
activeMint={activeMint}
163163
onSetActive={onMintClick}
164+
balance={selectedMintUrl ? balance?.[selectedMintUrl] : undefined}
164165
/>
165166
</div>
166167
)

src/pages/wallet/cashu/SendDialog.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,11 @@ export default function SendDialog({
555555
<span>{error}</span>
556556
</div>
557557
)}
558+
{sendAmount > 0 && balance !== undefined && sendAmount > balance && !generatedToken && (
559+
<div className="alert alert-error">
560+
<span>Amount exceeds balance ({balance} bit available)</span>
561+
</div>
562+
)}
558563
{!generatedToken ? (
559564
<form
560565
onSubmit={(e) => {
@@ -703,6 +708,13 @@ export default function SendDialog({
703708
<span>{error}</span>
704709
</div>
705710
)}
711+
{balance !== undefined &&
712+
((isLightningAddress && sendAmount > 0 && sendAmount > balance) ||
713+
(!isLightningAddress && invoiceAmount !== null && invoiceAmount > balance)) && (
714+
<div className="alert alert-error">
715+
<span>Amount exceeds balance ({balance} bit available)</span>
716+
</div>
717+
)}
706718
<div className="form-control">
707719
<label className="label">
708720
<span className="label-text">Search users</span>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import Modal from "@/shared/components/ui/Modal"
2+
import type {EnrichedHistoryEntry} from "../CashuWallet"
3+
import {getTransactionAmount, formatDate, formatUsd} from "./utils"
4+
import {Avatar} from "@/shared/components/user/Avatar"
5+
import {Name} from "@/shared/components/user/Name"
6+
import {RiBitCoinFill, RiFlashlightFill} from "@remixicon/react"
7+
import {useCashuWalletStore} from "@/stores/cashuWallet"
8+
9+
interface TransactionDetailsModalProps {
10+
isOpen: boolean
11+
onClose: () => void
12+
entry: EnrichedHistoryEntry | null
13+
usdRate: number | null
14+
}
15+
16+
export default function TransactionDetailsModal({
17+
isOpen,
18+
onClose,
19+
entry,
20+
usdRate,
21+
}: TransactionDetailsModalProps) {
22+
const {mintInfoCache} = useCashuWalletStore()
23+
24+
if (!isOpen || !entry) return null
25+
26+
const amount = getTransactionAmount(entry)
27+
const isLightning = entry.type === "mint" || entry.type === "melt"
28+
const isReceive = amount > 0
29+
const mintInfo = mintInfoCache[entry.mintUrl]?.info
30+
31+
return (
32+
<Modal onClose={onClose}>
33+
<div className="p-4 min-w-[400px]">
34+
<div className="space-y-6">
35+
{/* Header */}
36+
<div className="text-center">
37+
<div className="flex justify-center mb-4">
38+
{entry.paymentMetadata?.sender ? (
39+
<Avatar pubKey={entry.paymentMetadata.sender} width={64} />
40+
) : isLightning ? (
41+
<div className="w-16 h-16 rounded-full bg-accent/20 flex items-center justify-center">
42+
<RiFlashlightFill className="w-8 h-8 text-accent" />
43+
</div>
44+
) : (
45+
<div className="w-16 h-16 rounded-full bg-warning/20 flex items-center justify-center">
46+
<RiBitCoinFill className="w-8 h-8 text-warning" />
47+
</div>
48+
)}
49+
</div>
50+
<h3 className="text-2xl font-bold mb-2">
51+
{isReceive ? "Received" : "Sent"}
52+
</h3>
53+
<div className="text-3xl font-bold">
54+
{amount >= 0 ? "+" : ""}
55+
{amount} bit
56+
</div>
57+
<div className="text-base-content/60">
58+
{formatUsd(Math.abs(amount), usdRate)}
59+
</div>
60+
</div>
61+
62+
{/* Sender/Recipient */}
63+
{entry.paymentMetadata?.sender && isReceive && (
64+
<div>
65+
<h4 className="font-bold mb-2">FROM</h4>
66+
<div className="flex items-center gap-2">
67+
<Avatar pubKey={entry.paymentMetadata.sender} width={32} />
68+
<Name pubKey={entry.paymentMetadata.sender} />
69+
</div>
70+
</div>
71+
)}
72+
73+
{entry.paymentMetadata?.recipient && !isReceive && (
74+
<div>
75+
<h4 className="font-bold mb-2">TO</h4>
76+
<div className="flex items-center gap-2">
77+
<Avatar pubKey={entry.paymentMetadata.recipient} width={32} />
78+
<Name pubKey={entry.paymentMetadata.recipient} />
79+
</div>
80+
</div>
81+
)}
82+
83+
{/* Message */}
84+
{entry.paymentMetadata?.message && (
85+
<div>
86+
<h4 className="font-bold mb-2">MESSAGE</h4>
87+
<div className="text-sm text-base-content/80 break-words">
88+
&ldquo;{entry.paymentMetadata.message}&rdquo;
89+
</div>
90+
</div>
91+
)}
92+
93+
{/* Details */}
94+
<div>
95+
<h4 className="font-bold mb-2">DETAILS</h4>
96+
<div className="space-y-2">
97+
<div className="flex justify-between">
98+
<span className="text-sm text-base-content/60">Type</span>
99+
<span className="text-sm font-medium">
100+
{isLightning ? "Lightning" : "Ecash"}
101+
</span>
102+
</div>
103+
<div className="flex justify-between">
104+
<span className="text-sm text-base-content/60">Date</span>
105+
<span className="text-sm font-medium">{formatDate(entry.createdAt)}</span>
106+
</div>
107+
<div className="flex justify-between">
108+
<span className="text-sm text-base-content/60">Mint</span>
109+
<div className="text-sm font-medium text-right truncate max-w-[200px]">
110+
{mintInfo?.name || entry.mintUrl.replace(/^https?:\/\//, "")}
111+
</div>
112+
</div>
113+
{entry.paymentMetadata?.destination && (
114+
<div className="flex justify-between gap-2">
115+
<span className="text-sm text-base-content/60">Destination</span>
116+
<div className="text-sm font-medium text-right break-all max-w-[200px]">
117+
{entry.paymentMetadata.destination.toLowerCase().startsWith("lnurl")
118+
? entry.paymentMetadata.destination.slice(0, 20) + "..."
119+
: entry.paymentMetadata.destination}
120+
</div>
121+
</div>
122+
)}
123+
</div>
124+
</div>
125+
126+
<button onClick={onClose} className="btn btn-ghost w-full">
127+
Close
128+
</button>
129+
</div>
130+
</div>
131+
</Modal>
132+
)
133+
}

src/shared/components/embed/CashuToken.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ function CashuTokenComponent({match, key, event}: EmbedComponentProps) {
337337
{canRedeem && (
338338
<button
339339
onClick={handleRedeem}
340-
className="text-xs text-base-content/50 hover:text-base-content/70 underline disabled:opacity-50"
340+
className="text-xs text-base-content/50 hover:text-base-content/70 underline"
341341
disabled={loading || !!error || redeeming || redeemed || tokenSpent}
342342
>
343343
{redeeming ? "Redeeming..." : redeemed ? "Redeemed" : "Redeem"}
@@ -357,7 +357,7 @@ function CashuTokenComponent({match, key, event}: EmbedComponentProps) {
357357
{canRedeem ? (
358358
<button
359359
onClick={handleRedeem}
360-
className="btn btn-primary btn-sm flex-1 disabled:opacity-50"
360+
className="btn btn-primary btn-sm flex-1"
361361
disabled={loading || !!error || redeeming || redeemed}
362362
>
363363
{redeeming ? "Redeeming..." : "Redeem"}

0 commit comments

Comments
 (0)