mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 18:21:28 +02:00
feat: detect and display zap payments in NWC wallet viewer (#140)
* feat: detect and display zap payments in NWC wallet viewer Add intelligent zap payment detection and enhanced display in the NWC wallet transaction list and detail views. Changes: - Add wallet-utils.ts with zap request parsing (kind 9734 detection) - Parse zap requests from transaction descriptions with LRU caching (500 entry limit) - Display username + message preview in transaction list with ⚡ indicator - Show full zap details in transaction detail dialog: - Zapper name (clickable UserName component) - Full zap message with RichText formatting - Zapped post rendered inline using KindRenderer - Loading states for event fetching - Follow React Hooks rules (unconditional hook calls) - Type-safe implementation with proper pointer handling Technical notes: - parseZapRequest() extracts sender, message, and event pointers from JSON-embedded zap requests - Caching prevents redundant JSON parsing on re-renders - Supports both event IDs (e tag) and address coordinates (a tag) - parseAddressCoordinate() handles kind:pubkey:identifier format * refactor: use applesauce caching pattern and RichText for zaps Improvements: - Replace Map-based cache with getOrComputeCachedValue pattern - Cache parsed zap requests on transaction objects using Symbol - Follows applesauce convention for computed value caching - More memory-efficient than global Map cache - Use RichText component for zap messages in transaction list - Supports links, mentions, and other rich formatting - CSS truncation instead of JS string manipulation - Update parseZapRequest to accept transaction object instead of description string - Enables proper caching on the transaction object - Cleaner API surface Technical changes: - Remove getZapMessagePreview() helper (now using CSS truncate) - Add getOrComputeCachedValue import from applesauce-core/helpers - Update all parseZapRequest call sites to pass transaction object - Wrap zap message in RichText component for proper formatting * feat: enhance zap display with RichText context and scrollable details Improvements: - Pass zap request event as context to RichText components - Enables proper mention/link resolution in zap messages - Supports interactive elements (mentions, hashtags, links) - Provides full event context for rendering - Make transaction detail dialog scrollable - Add max-h-[90vh] to DialogContent with flex layout - Wrap content in overflow-y-auto container with max-h-[calc(90vh-8rem)] - Prevents dialog overflow when displaying large zapped posts - Smooth scrolling for long zap message threads - Reduce transaction page size from 20 to 10 - Better performance with rich zap rendering - Faster initial load and scroll rendering - Reduces memory footprint for transaction list Technical changes: - Add zapRequestEvent field to ZapRequestInfo interface - Pass zapRequestEvent to all RichText components rendering zap messages - Update BATCH_SIZE constant from 20 to 10 - Add flex layout to DialogContent for proper scrolling - Add pr-2 padding to scrollable container for visual spacing * revert: restore BATCH_SIZE to 20 transactions * refactor: improve zap transaction list item UI Changes: - Use UserName component for zapper display (applies accent color) - Remove colon separator between username and message - Keep username and message on single line with proper truncation - Remove unused imports (getDisplayName, useProfile) - Reduce transaction detail dialog max height from 90vh to 70vh - More compact display for better UX - Prevents excessive white space UI improvements: - Zap icon + UserName (accent color) + message all on one line - UserName component is flex-shrink-0 to prevent squishing - Message text truncates with CSS overflow - Cleaner, more compact visual hierarchy * fix: improve spacing and truncation in zap transaction items - Increase gap between username and message from gap-1 to gap-2 (0.5rem) - Add min-w-0 to message span for proper ellipsis truncation in flex - Remove duplicate truncate class from parent div to prevent conflicts - Message now properly shows ellipsis (...) when it doesn't fit on one line * feat: add line-clamp and expandable raw transaction view Changes: - Replace truncate with line-clamp-1 on zap message for proper single-line clamping - Add expandable 'Show Raw Transaction' section in transaction detail dialog - Collapsible with ChevronRight/ChevronDown icons - Shows JSON.stringify(transaction, null, 2) in scrollable pre block - Uses CodeCopyButton component for consistent copy UX - Max height 60 (15rem) with overflow-y-auto for long transactions - Add state management for raw transaction expansion and copy status - Reset raw transaction state when dialog closes UI improvements: - Clean expansion interaction with hover effects - Properly formatted JSON with 2-space indentation - Accessible copy button with aria-label - Auto-collapses when closing the dialog * feat: parse zap requests from invoice description as fallback Enhance zap request parsing to check multiple sources: - First try transaction.description (primary source) - If not found, decode the Lightning invoice and check its description field - This handles cases where the zap request is embedded in the invoice Changes: - Extract parsing logic into tryParseZapRequestJson() helper - Add invoice field to parseZapRequest() transaction parameter - Import light-bolt11-decoder to decode invoices - Try invoice description as fallback when tx description doesn't contain zap - Maintain applesauce caching pattern on transaction object This ensures zap payments are detected and displayed correctly regardless of where the zap request JSON is stored (tx description vs invoice description). --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
ArrowDownLeft,
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useWallet } from "@/hooks/useWallet";
|
||||
@@ -49,6 +50,13 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import ConnectWalletDialog from "./ConnectWalletDialog";
|
||||
import { RelayLink } from "@/components/nostr/RelayLink";
|
||||
import { parseZapRequest } from "@/lib/wallet-utils";
|
||||
import { Zap } from "lucide-react";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { KindRenderer } from "./nostr/kinds";
|
||||
import { RichText } from "./nostr/RichText";
|
||||
import { UserName } from "./nostr/UserName";
|
||||
import { CodeCopyButton } from "./CodeCopyButton";
|
||||
|
||||
interface Transaction {
|
||||
type: "incoming" | "outgoing";
|
||||
@@ -206,6 +214,132 @@ function parseInvoice(invoice: string): InvoiceDetails | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse coordinate string (kind:pubkey:identifier)
|
||||
*/
|
||||
function parseAddressCoordinate(
|
||||
coordinate: string,
|
||||
): { kind: number; pubkey: string; identifier: string } | null {
|
||||
const parts = coordinate.split(":");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const kind = parseInt(parts[0], 10);
|
||||
if (isNaN(kind)) return null;
|
||||
|
||||
return {
|
||||
kind,
|
||||
pubkey: parts[1],
|
||||
identifier: parts[2],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to render zap details in the transaction detail dialog
|
||||
*/
|
||||
function ZapTransactionDetail({ transaction }: { transaction: Transaction }) {
|
||||
const zapInfo = parseZapRequest(transaction);
|
||||
|
||||
// Parse address coordinate if present (format: kind:pubkey:identifier)
|
||||
const addressPointer = zapInfo?.zappedEventAddress
|
||||
? parseAddressCoordinate(zapInfo.zappedEventAddress)
|
||||
: null;
|
||||
|
||||
// Call hooks unconditionally (before early return)
|
||||
const zappedEvent = useNostrEvent(
|
||||
zapInfo?.zappedEventId
|
||||
? { id: zapInfo.zappedEventId }
|
||||
: addressPointer || undefined,
|
||||
);
|
||||
|
||||
// Early return after hooks
|
||||
if (!zapInfo) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
{/* Zap sender */}
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Zap className="size-3 fill-zap text-zap" />
|
||||
Zap From
|
||||
</Label>
|
||||
<div className="mt-1">
|
||||
<UserName pubkey={zapInfo.sender} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zap message */}
|
||||
{zapInfo.message && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Zap Message</Label>
|
||||
<div className="mt-1 text-sm">
|
||||
<RichText
|
||||
content={zapInfo.message}
|
||||
event={zapInfo.zapRequestEvent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zapped event */}
|
||||
{zappedEvent && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Zapped Post</Label>
|
||||
<div className="mt-1 border border-muted rounded-md overflow-hidden">
|
||||
<KindRenderer event={zappedEvent} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading state for zapped event */}
|
||||
{(zapInfo.zappedEventId || zapInfo.zappedEventAddress) &&
|
||||
!zappedEvent && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Zapped Post</Label>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Loading event...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to render a transaction row with zap detection
|
||||
*/
|
||||
function TransactionLabel({ transaction }: { transaction: Transaction }) {
|
||||
const zapInfo = parseZapRequest(transaction);
|
||||
|
||||
// Not a zap - use original description or default label
|
||||
if (!zapInfo) {
|
||||
return (
|
||||
<span className="text-sm truncate">
|
||||
{transaction.description ||
|
||||
(transaction.type === "incoming" ? "Received" : "Payment")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// It's a zap! Show username + message on one line
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Zap className="size-3.5 flex-shrink-0 fill-zap text-zap" />
|
||||
<div className="text-sm min-w-0 flex items-center gap-2">
|
||||
<UserName pubkey={zapInfo.sender} className="flex-shrink-0" />
|
||||
{zapInfo.message && (
|
||||
<span className="line-clamp-1 min-w-0">
|
||||
<RichText
|
||||
content={zapInfo.message}
|
||||
event={zapInfo.zapRequestEvent}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WalletViewer() {
|
||||
const { state, disconnectNWC: disconnectNWCFromState } = useGrimoire();
|
||||
const {
|
||||
@@ -262,6 +396,8 @@ export default function WalletViewer() {
|
||||
const [selectedTransaction, setSelectedTransaction] =
|
||||
useState<Transaction | null>(null);
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
||||
const [showRawTransaction, setShowRawTransaction] = useState(false);
|
||||
const [copiedRawTx, setCopiedRawTx] = useState(false);
|
||||
|
||||
// Load wallet info when connected
|
||||
useEffect(() => {
|
||||
@@ -363,7 +499,7 @@ export default function WalletViewer() {
|
||||
// Reload transactions
|
||||
reloadTransactions();
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore errors, will retry
|
||||
} finally {
|
||||
setCheckingPayment(false);
|
||||
@@ -985,9 +1121,6 @@ export default function WalletViewer() {
|
||||
}
|
||||
|
||||
const tx = item.data;
|
||||
const txLabel =
|
||||
tx.description ||
|
||||
(tx.type === "incoming" ? "Received" : "Payment");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1001,7 +1134,7 @@ export default function WalletViewer() {
|
||||
) : (
|
||||
<ArrowUpRight className="size-4 text-red-500 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm truncate">{txLabel}</span>
|
||||
<TransactionLabel transaction={tx} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 ml-4">
|
||||
<p className="text-sm font-semibold font-mono">
|
||||
@@ -1062,90 +1195,149 @@ export default function WalletViewer() {
|
||||
</Dialog>
|
||||
|
||||
{/* Transaction Detail Dialog */}
|
||||
<Dialog open={detailDialogOpen} onOpenChange={setDetailDialogOpen}>
|
||||
<DialogContent>
|
||||
<Dialog
|
||||
open={detailDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDetailDialogOpen(open);
|
||||
if (!open) {
|
||||
setShowRawTransaction(false);
|
||||
setCopiedRawTx(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-h-[70vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Transaction Details</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedTransaction && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{selectedTransaction.type === "incoming" ? (
|
||||
<ArrowDownLeft className="size-6 text-green-500" />
|
||||
) : (
|
||||
<ArrowUpRight className="size-6 text-red-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-lg font-semibold">
|
||||
{selectedTransaction.type === "incoming"
|
||||
? "Received"
|
||||
: "Sent"}
|
||||
</p>
|
||||
<p className="text-2xl font-bold font-mono">
|
||||
{formatSats(selectedTransaction.amount)} sats
|
||||
</p>
|
||||
<div className="overflow-y-auto max-h-[calc(70vh-8rem)] pr-2">
|
||||
{selectedTransaction && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{selectedTransaction.type === "incoming" ? (
|
||||
<ArrowDownLeft className="size-6 text-green-500" />
|
||||
) : (
|
||||
<ArrowUpRight className="size-6 text-red-500" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-lg font-semibold">
|
||||
{selectedTransaction.type === "incoming"
|
||||
? "Received"
|
||||
: "Sent"}
|
||||
</p>
|
||||
<p className="text-2xl font-bold font-mono">
|
||||
{formatSats(selectedTransaction.amount)} sats
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{selectedTransaction.description && (
|
||||
<div className="space-y-2">
|
||||
{selectedTransaction.description &&
|
||||
!parseZapRequest(selectedTransaction) && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Description
|
||||
</Label>
|
||||
<p className="text-sm">
|
||||
{selectedTransaction.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Description
|
||||
Date
|
||||
</Label>
|
||||
<p className="text-sm">{selectedTransaction.description}</p>
|
||||
<p className="text-sm font-mono">
|
||||
{formatFullDate(selectedTransaction.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Date</Label>
|
||||
<p className="text-sm font-mono">
|
||||
{formatFullDate(selectedTransaction.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
{selectedTransaction.fees_paid !== undefined &&
|
||||
selectedTransaction.fees_paid > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Fees Paid
|
||||
</Label>
|
||||
<p className="text-sm font-mono">
|
||||
{formatSats(selectedTransaction.fees_paid)} sats
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTransaction.fees_paid !== undefined &&
|
||||
selectedTransaction.fees_paid > 0 && (
|
||||
{selectedTransaction.payment_hash && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Fees Paid
|
||||
Payment Hash
|
||||
</Label>
|
||||
<p className="text-sm font-mono">
|
||||
{formatSats(selectedTransaction.fees_paid)} sats
|
||||
<p className="text-xs font-mono break-all bg-muted p-2 rounded">
|
||||
{selectedTransaction.payment_hash}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTransaction.payment_hash && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Payment Hash
|
||||
</Label>
|
||||
<p className="text-xs font-mono break-all bg-muted p-2 rounded">
|
||||
{selectedTransaction.payment_hash}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedTransaction.preimage && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Preimage
|
||||
</Label>
|
||||
<p className="text-xs font-mono break-all bg-muted p-2 rounded">
|
||||
{selectedTransaction.preimage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedTransaction.preimage && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Preimage
|
||||
</Label>
|
||||
<p className="text-xs font-mono break-all bg-muted p-2 rounded">
|
||||
{selectedTransaction.preimage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* Zap Details (if this is a zap payment) */}
|
||||
<ZapTransactionDetail transaction={selectedTransaction} />
|
||||
|
||||
{/* Raw Transaction (expandable) */}
|
||||
<div className="border-t border-border pt-4 mt-4">
|
||||
<button
|
||||
onClick={() => setShowRawTransaction(!showRawTransaction)}
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full"
|
||||
>
|
||||
{showRawTransaction ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
<span>Show Raw Transaction</span>
|
||||
</button>
|
||||
|
||||
{showRawTransaction && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="relative">
|
||||
<pre className="text-xs font-mono bg-muted p-3 rounded overflow-x-auto max-h-60 overflow-y-auto">
|
||||
{JSON.stringify(selectedTransaction, null, 2)}
|
||||
</pre>
|
||||
<CodeCopyButton
|
||||
copied={copiedRawTx}
|
||||
onCopy={() => {
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify(selectedTransaction, null, 2),
|
||||
);
|
||||
setCopiedRawTx(true);
|
||||
setTimeout(() => setCopiedRawTx(false), 2000);
|
||||
}}
|
||||
label="Copy transaction JSON"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDetailDialogOpen(false)}
|
||||
onClick={() => {
|
||||
setDetailDialogOpen(false);
|
||||
setShowRawTransaction(false);
|
||||
setCopiedRawTx(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
121
src/lib/wallet-utils.ts
Normal file
121
src/lib/wallet-utils.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Wallet Utilities
|
||||
*
|
||||
* Helper functions for working with wallet transactions and zap payments
|
||||
*/
|
||||
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { getOrComputeCachedValue } from "applesauce-core/helpers";
|
||||
import { decode as decodeBolt11 } from "light-bolt11-decoder";
|
||||
|
||||
export interface ZapRequestInfo {
|
||||
sender: string; // pubkey of the zapper
|
||||
message: string; // zap message content
|
||||
zappedEventId?: string; // ID of the zapped event (from e tag)
|
||||
zappedEventAddress?: string; // Address of the zapped event (from a tag)
|
||||
amount?: number; // amount in sats (if available)
|
||||
zapRequestEvent: NostrEvent; // The full kind 9734 zap request event
|
||||
}
|
||||
|
||||
// Symbol for caching parsed zap requests on transaction objects
|
||||
const ZapRequestSymbol = Symbol("zapRequest");
|
||||
|
||||
/**
|
||||
* Try to parse a zap request JSON string into a ZapRequestInfo object
|
||||
* @param jsonString - The JSON string to parse
|
||||
* @returns ZapRequestInfo if valid zap request, null otherwise
|
||||
*/
|
||||
function tryParseZapRequestJson(jsonString: string): ZapRequestInfo | null {
|
||||
try {
|
||||
// Try to parse as JSON
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
// Check if it's a valid zap request (kind 9734)
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== "object" ||
|
||||
parsed.kind !== 9734 ||
|
||||
!parsed.pubkey ||
|
||||
typeof parsed.pubkey !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = parsed as NostrEvent;
|
||||
|
||||
// Extract zapped event from tags
|
||||
let zappedEventId: string | undefined;
|
||||
let zappedEventAddress: string | undefined;
|
||||
|
||||
if (Array.isArray(event.tags)) {
|
||||
// Look for e tag (event ID)
|
||||
const eTag = event.tags.find(
|
||||
(tag) => Array.isArray(tag) && tag.length >= 2 && tag[0] === "e",
|
||||
);
|
||||
if (eTag && typeof eTag[1] === "string") {
|
||||
zappedEventId = eTag[1];
|
||||
}
|
||||
|
||||
// Look for a tag (address/coordinate)
|
||||
const aTag = event.tags.find(
|
||||
(tag) => Array.isArray(tag) && tag.length >= 2 && tag[0] === "a",
|
||||
);
|
||||
if (aTag && typeof aTag[1] === "string") {
|
||||
zappedEventAddress = aTag[1];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sender: event.pubkey,
|
||||
message: event.content || "",
|
||||
zappedEventId,
|
||||
zappedEventAddress,
|
||||
zapRequestEvent: event,
|
||||
};
|
||||
} catch {
|
||||
// Not JSON or parsing failed - not a zap request
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse a zap request from a transaction
|
||||
* Checks both the transaction description and the invoice description
|
||||
* Transaction descriptions for zaps contain a JSON-stringified kind 9734 event
|
||||
* Results are cached on the transaction object using applesauce pattern
|
||||
*
|
||||
* @param transaction - The transaction object with description and/or invoice field
|
||||
* @returns ZapRequestInfo if this is a zap payment, null otherwise
|
||||
*/
|
||||
export function parseZapRequest(transaction: {
|
||||
description?: string;
|
||||
invoice?: string;
|
||||
}): ZapRequestInfo | null {
|
||||
// Use applesauce caching pattern - cache result on transaction object
|
||||
return getOrComputeCachedValue(transaction, ZapRequestSymbol, () => {
|
||||
// Try parsing the transaction description first
|
||||
if (transaction.description) {
|
||||
const result = tryParseZapRequestJson(transaction.description);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
// If that didn't work, try decoding the invoice and checking its description
|
||||
if (transaction.invoice) {
|
||||
try {
|
||||
const decoded = decodeBolt11(transaction.invoice);
|
||||
const descSection = decoded.sections.find(
|
||||
(s) => s.name === "description",
|
||||
);
|
||||
|
||||
if (descSection && descSection.value) {
|
||||
const result = tryParseZapRequestJson(descSection.value as string);
|
||||
if (result) return result;
|
||||
}
|
||||
} catch {
|
||||
// Invoice decoding failed, ignore
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user