Skip to content

Commit e3da603

Browse files
committed
mint metadata & listing
1 parent 67bdf31 commit e3da603

File tree

9 files changed

+326
-72
lines changed

9 files changed

+326
-72
lines changed

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"plugins": {
2727
"deep-link": {
2828
"desktop": {
29-
"schemes": ["nostr"]
29+
"schemes": ["nostr", "lightning"]
3030
}
3131
}
3232
},

src/main.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,16 @@ if (isTauri()) {
183183
const url = urls[0]
184184
console.log("Deep link opened:", url)
185185

186-
// Strip protocol and navigate
186+
// Handle lightning: protocol
187+
if (url.startsWith("lightning:")) {
188+
const invoice = url.replace(/^lightning:/, "")
189+
// Navigate to wallet with invoice in state
190+
window.history.pushState({lightningInvoice: invoice}, "", "/wallet")
191+
window.dispatchEvent(new PopStateEvent("popstate"))
192+
return
193+
}
194+
195+
// Strip protocol and navigate for nostr: links
187196
const path = url.replace(/^(nostr:|web\+nostr:)/, "")
188197
window.history.pushState({}, "", `/${path}`)
189198
window.dispatchEvent(new PopStateEvent("popstate"))

src/pages/wallet/CashuWallet.tsx

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useState, useEffect} from "react"
1+
import {useState, useEffect, useCallback} from "react"
22
import {initCashuManager, getCashuManager} from "@/lib/cashu/manager"
33
import type {Manager} from "@/lib/cashu/core/index"
44
import type {HistoryEntry, SendHistoryEntry} from "@/lib/cashu/core/models/History"
@@ -48,8 +48,16 @@ const ensureMeltQuoteReposInit = async () => {
4848
const DEFAULT_MINT = "https://mint.coinos.io"
4949

5050
export default function CashuWallet() {
51-
const {expandHistory, activeTab, toggleExpandHistory, setActiveTab} =
52-
useCashuWalletStore()
51+
const {
52+
expandHistory,
53+
activeTab,
54+
activeMint,
55+
toggleExpandHistory,
56+
setActiveTab,
57+
setActiveMint,
58+
setCachedMintInfo,
59+
getCachedMintInfo,
60+
} = useCashuWalletStore()
5361
const {activeProviderType} = useWalletProviderStore()
5462
const navigate = useNavigate()
5563
const location = useLocation()
@@ -69,6 +77,7 @@ export default function CashuWallet() {
6977
)
7078
const [sendDialogInitialInvoice, setSendDialogInitialInvoice] = useState<string>("")
7179
const [receiveDialogInitialToken, setReceiveDialogInitialToken] = useState<string>("")
80+
const [receiveDialogInitialInvoice, setReceiveDialogInitialInvoice] = useState<string>("")
7281
const [refreshing, setRefreshing] = useState(false)
7382
const [qrError, setQrError] = useState<string>("")
7483
const [isOffline, setIsOffline] = useState(!navigator.onLine)
@@ -186,10 +195,18 @@ export default function CashuWallet() {
186195
}
187196
}
188197

189-
const handleSendEntryClick = (entry: SendHistoryEntry) => {
198+
const handleSendEntryClick = useCallback((entry: SendHistoryEntry) => {
190199
setSendDialogInitialToken(entry.token)
191200
setShowSendDialog(true)
192-
}
201+
}, [])
202+
203+
const handleMintEntryClick = useCallback((entry: HistoryEntry) => {
204+
if (entry.type === "mint" && entry.state === "UNPAID") {
205+
// Show receive dialog with the pending invoice
206+
setReceiveDialogInitialInvoice(entry.paymentRequest)
207+
setShowReceiveDialog(true)
208+
}
209+
}, [])
193210

194211
const handleCloseSendDialog = () => {
195212
setShowSendDialog(false)
@@ -200,6 +217,7 @@ export default function CashuWallet() {
200217
const handleCloseReceiveDialog = () => {
201218
setShowReceiveDialog(false)
202219
setReceiveDialogInitialToken("")
220+
setReceiveDialogInitialInvoice("")
203221
}
204222

205223
const handleRefresh = async () => {
@@ -330,11 +348,17 @@ export default function CashuWallet() {
330348

331349
// Handle receiveToken from navigation state
332350
useEffect(() => {
333-
const state = location.state as {receiveToken?: string} | undefined
351+
const state = location.state as {receiveToken?: string; lightningInvoice?: string} | undefined
334352
if (state?.receiveToken && manager) {
335353
setReceiveDialogInitialToken(state.receiveToken)
336354
setShowReceiveDialog(true)
337355
}
356+
if (state?.lightningInvoice && manager) {
357+
setSendDialogInitialInvoice(state.lightningInvoice)
358+
setShowSendDialog(true)
359+
// Clear state after handling
360+
window.history.replaceState({}, "", "/wallet")
361+
}
338362
}, [location.state, manager])
339363

340364
// Handle paymentRequest from URL params
@@ -392,6 +416,28 @@ export default function CashuWallet() {
392416
const bal = await mgr.wallet.getBalances()
393417
setBalance(bal)
394418

419+
// Load all mints and cache their info
420+
const mints = await mgr.mint.getAllMints()
421+
for (const {mintUrl} of mints) {
422+
// Skip if already cached
423+
if (getCachedMintInfo(mintUrl)) continue
424+
425+
// Fetch and cache mint info
426+
try {
427+
const {CashuMint} = await import("@cashu/cashu-ts")
428+
const mint = new CashuMint(mintUrl)
429+
const info = await mint.getInfo()
430+
setCachedMintInfo(mintUrl, info)
431+
} catch (err) {
432+
console.warn(`Failed to fetch info for ${mintUrl}:`, err)
433+
}
434+
}
435+
436+
// Set active mint if not already set
437+
if (!activeMint && bal && Object.keys(bal).length > 0) {
438+
setActiveMint(Object.keys(bal)[0])
439+
}
440+
395441
// Load history
396442
const hist = await mgr.history.getPaginatedHistory(0, 1000)
397443
const enrichedHist = await enrichHistoryWithMetadata(hist)
@@ -552,12 +598,15 @@ export default function CashuWallet() {
552598
</div>
553599

554600
{/* Mint Info */}
555-
{balance && Object.keys(balance).length > 0 && (
601+
{activeMint && (
556602
<div className="text-sm text-base-content/60 mt-4">
557603
Mint:{" "}
558-
<span className="font-medium">
559-
{Object.keys(balance)[0].replace(/^https?:\/\//, "")}
560-
</span>
604+
<button
605+
onClick={() => setActiveTab("mints")}
606+
className="font-medium hover:underline cursor-pointer"
607+
>
608+
{activeMint.replace(/^https?:\/\//, "")}
609+
</button>
561610
</div>
562611
)}
563612
</div>
@@ -654,6 +703,7 @@ export default function CashuWallet() {
654703
history={history}
655704
usdRate={usdRate}
656705
onSendEntryClick={handleSendEntryClick}
706+
onMintEntryClick={handleMintEntryClick}
657707
/>
658708
</div>
659709
)}
@@ -665,6 +715,8 @@ export default function CashuWallet() {
665715
balance={balance}
666716
manager={manager}
667717
onBalanceUpdate={refreshData}
718+
activeMint={activeMint}
719+
onMintClick={setActiveMint}
668720
/>
669721
</div>
670722
)}
@@ -676,7 +728,7 @@ export default function CashuWallet() {
676728
isOpen={showSendDialog}
677729
onClose={handleCloseSendDialog}
678730
manager={manager}
679-
mintUrl={balance ? Object.keys(balance)[0] : DEFAULT_MINT}
731+
mintUrl={activeMint || DEFAULT_MINT}
680732
onSuccess={refreshData}
681733
initialToken={sendDialogInitialToken}
682734
initialInvoice={sendDialogInitialInvoice}
@@ -687,9 +739,10 @@ export default function CashuWallet() {
687739
isOpen={showReceiveDialog}
688740
onClose={handleCloseReceiveDialog}
689741
manager={manager}
690-
mintUrl={balance ? Object.keys(balance)[0] : DEFAULT_MINT}
742+
mintUrl={activeMint || DEFAULT_MINT}
691743
onSuccess={refreshData}
692744
initialToken={receiveDialogInitialToken}
745+
initialInvoice={receiveDialogInitialInvoice}
693746
balance={totalBalance}
694747
onScanRequest={() => setShowQRScanner(true)}
695748
/>

src/pages/wallet/cashu/HistoryList.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {SendHistoryEntry} from "@/lib/cashu/core/models/History"
1+
import type {SendHistoryEntry, HistoryEntry} from "@/lib/cashu/core/models/History"
22
import type {EnrichedHistoryEntry} from "../CashuWallet"
33
import {RiFlashlightFill, RiBitCoinFill} from "@remixicon/react"
44
import {useState} from "react"
@@ -16,12 +16,14 @@ interface HistoryListProps {
1616
history: EnrichedHistoryEntry[]
1717
usdRate: number | null
1818
onSendEntryClick?: (entry: SendHistoryEntry) => void
19+
onMintEntryClick?: (entry: HistoryEntry) => void
1920
}
2021

2122
export default function HistoryList({
2223
history,
2324
usdRate,
2425
onSendEntryClick,
26+
onMintEntryClick,
2527
}: HistoryListProps) {
2628
const [displayCount, setDisplayCount] = useState(INITIAL_DISPLAY)
2729
const navigate = useNavigate()
@@ -44,19 +46,26 @@ export default function HistoryList({
4446
const amount = getTransactionAmount(entry)
4547
const status = getTransactionStatus(entry)
4648
const isSend = entry.type === "send"
49+
const isPendingMint = entry.type === "mint" && status === "pending"
4750
const isZapWithEvent =
4851
entry.paymentMetadata?.type === "zap" && entry.paymentMetadata?.eventId
4952
const hasRecipient = !!entry.paymentMetadata?.recipient
5053
const hasSender = !!entry.paymentMetadata?.sender
5154
const isClickable =
52-
(isSend && onSendEntryClick) || isZapWithEvent || hasRecipient || hasSender
55+
(isSend && onSendEntryClick) ||
56+
(isPendingMint && onMintEntryClick) ||
57+
isZapWithEvent ||
58+
hasRecipient ||
59+
hasSender
5360

5461
// Determine if it's Lightning (mint/melt) or Ecash (send/receive)
5562
const label =
5663
entry.type === "mint" || entry.type === "melt" ? "Lightning" : "Ecash"
5764

5865
const handleClick = () => {
59-
if (isZapWithEvent && entry.paymentMetadata?.eventId) {
66+
if (isPendingMint && onMintEntryClick) {
67+
onMintEntryClick(entry)
68+
} else if (isZapWithEvent && entry.paymentMetadata?.eventId) {
6069
navigate(`/${nip19.noteEncode(entry.paymentMetadata.eventId)}`)
6170
} else if (hasRecipient && entry.paymentMetadata?.recipient) {
6271
// Lightning payments (melt): Navigate to profile

src/pages/wallet/cashu/MintDetailsModal.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {useState, useEffect} from "react"
22
import Modal from "@/shared/components/ui/Modal"
33
import type {Manager} from "@/lib/cashu/core/index"
4-
import {RiDeleteBinLine, RiFileCopyLine} from "@remixicon/react"
4+
import {RiDeleteBinLine, RiFileCopyLine, RiRefreshLine} from "@remixicon/react"
55
import {confirm} from "@/utils/utils"
6+
import {useCashuWalletStore} from "@/stores/cashuWallet"
67

78
interface MintInfo {
89
name?: string
@@ -21,6 +22,8 @@ interface MintDetailsModalProps {
2122
mintUrl: string
2223
manager: Manager | null
2324
onMintDeleted: () => void
25+
activeMint: string | null
26+
onSetActive: (mintUrl: string) => void
2427
}
2528

2629
export default function MintDetailsModal({
@@ -29,21 +32,35 @@ export default function MintDetailsModal({
2932
mintUrl,
3033
manager,
3134
onMintDeleted,
35+
activeMint,
36+
onSetActive,
3237
}: MintDetailsModalProps) {
38+
const {getCachedMintInfo, setCachedMintInfo, clearMintInfoCache} = useCashuWalletStore()
3339
const [mintInfo, setMintInfo] = useState<MintInfo | null>(null)
3440
const [qrCodeUrl, setQrCodeUrl] = useState<string>("")
3541
const [loading, setLoading] = useState(true)
3642
const [error, setError] = useState<string>("")
43+
const [refreshing, setRefreshing] = useState(false)
3744

3845
useEffect(() => {
3946
if (!isOpen || !mintUrl) return
4047

4148
const fetchMintInfo = async () => {
4249
setLoading(true)
4350
try {
51+
// Check cache first
52+
const cached = getCachedMintInfo(mintUrl)
53+
if (cached) {
54+
setMintInfo(cached as unknown as MintInfo)
55+
setLoading(false)
56+
return
57+
}
58+
59+
// Fetch from network
4460
const response = await fetch(`${mintUrl}/v1/info`)
4561
const data = await response.json()
4662
setMintInfo(data)
63+
setCachedMintInfo(mintUrl, data)
4764
} catch (error) {
4865
console.error("Failed to fetch mint info:", error)
4966
} finally {
@@ -81,7 +98,24 @@ export default function MintDetailsModal({
8198
}
8299
}
83100
generateQR()
84-
}, [isOpen, mintUrl])
101+
}, [isOpen, mintUrl, getCachedMintInfo, setCachedMintInfo])
102+
103+
const handleRefreshMetadata = async () => {
104+
setRefreshing(true)
105+
try {
106+
// Clear cache and fetch fresh data
107+
clearMintInfoCache(mintUrl)
108+
const response = await fetch(`${mintUrl}/v1/info`)
109+
const data = await response.json()
110+
setMintInfo(data)
111+
setCachedMintInfo(mintUrl, data)
112+
} catch (error) {
113+
console.error("Failed to refresh mint info:", error)
114+
setError("Failed to refresh metadata")
115+
} finally {
116+
setRefreshing(false)
117+
}
118+
}
85119

86120
const handleCopyUrl = () => {
87121
navigator.clipboard.writeText(mintUrl)
@@ -242,6 +276,22 @@ export default function MintDetailsModal({
242276
<div>
243277
<h3 className="font-bold mb-2">ACTIONS</h3>
244278
<div className="space-y-2">
279+
{activeMint !== mintUrl && (
280+
<button
281+
onClick={() => onSetActive(mintUrl)}
282+
className="btn btn-primary w-full justify-start"
283+
>
284+
Set as Active Mint
285+
</button>
286+
)}
287+
<button
288+
onClick={handleRefreshMetadata}
289+
className="btn btn-ghost w-full justify-start"
290+
disabled={refreshing}
291+
>
292+
<RiRefreshLine className={`w-5 h-5 mr-2 ${refreshing ? "animate-spin" : ""}`} />
293+
{refreshing ? "Refreshing..." : "Refresh metadata"}
294+
</button>
245295
<button
246296
onClick={handleCopyUrl}
247297
className="btn btn-ghost w-full justify-start"

0 commit comments

Comments
 (0)