Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Names from '@/pages/Names';
import NameProfile from '@/pages/NameProfile';
import NamesAuctions from '@/pages/NamesAuctions';
import Activity from '@/pages/Activity';
import History from '@/pages/History';
import Portfolio from '@/pages/Portfolio';
import Debug from '@/pages/Debug';
import { useChain } from '@/context/ChainContext';
Expand Down Expand Up @@ -223,7 +224,7 @@ export function App() {
<Route path="/n/:name" element={<NameProfile />} />
<Route path="/names/auctions" element={<NamesAuctions />} />
<Route path="/activity" element={<Activity />} />
<Route path="/history" element={<Activity />} />
<Route path="/history" element={<History />} />
<Route path="/portfolio" element={<Portfolio />} />
<Route path="/debug" element={<Debug />} />
<Route path="*" element={<Navigate to="/send" replace />} />
Expand Down
220 changes: 186 additions & 34 deletions src/pages/History.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,197 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useChain } from '@/context/ChainContext';
import { StellarHistory } from '@/components/StellarHistory';
import { ActivityRow } from '@/components/ActivityRow';
import { EmptyState } from '@/components/EmptyState';
import {
useActivityStore,
type ActivityEntry,
type ActivityKind,
type ActivityStatus,
} from '@/stores/activityStore';
import { downloadActivityCsv, downloadActivityJson } from '@/utils/activityExport';

type HistoryChain = 'all' | 'horizen' | 'stellar' | 'solana' | 'ckb';

const CHAINS: HistoryChain[] = ['all', 'horizen', 'stellar', 'solana', 'ckb'];

export default function History() {
const navigate = useNavigate();
const { chain } = useChain();
const { entries, clearAll } = useActivityStore();
const [filterKind, setFilterKind] = useState<ActivityKind | 'all'>('all');
const [filterStatus, setFilterStatus] = useState<ActivityStatus | 'all'>('all');
const [filterChain, setFilterChain] = useState<HistoryChain>('all');

const filteredEntries = useMemo(() => {
return entries
.filter((entry: ActivityEntry) => {
if (filterChain !== 'all' && entry.chain !== filterChain) return false;
if (filterKind !== 'all' && entry.kind !== filterKind) return false;
if (filterStatus !== 'all' && entry.status !== filterStatus) return false;
return true;
})
.sort((a: ActivityEntry, b: ActivityEntry) => b.timestamp - a.timestamp);
}, [entries, filterChain, filterKind, filterStatus]);

if (chain === 'stellar') return <StellarHistory />;
// Exports deliberately omit `metadata` so private key and memo material
// is never leaked into CSV or JSON downloads.
const exportEntries = useMemo(() => entries.map(({ metadata, ...rest }) => rest), [entries]);

return (
<div className="flex flex-col gap-6">
<section className="flex flex-col gap-3">
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
History
</h1>
</section>
<EmptyState
illustration={
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
<section className="flex flex-col gap-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Multi-Chain Activity
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Activity History
</h1>
</div>
<button
type="button"
onClick={() => clearAll()}
disabled={entries.length === 0}
className="rounded-lg bg-surface-container px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:bg-error/20 hover:text-error disabled:cursor-not-allowed disabled:opacity-40"
>
Clear History
</button>
</div>

<div className="flex flex-col gap-4 border border-outline-variant bg-surface-container p-4">
<div className="flex flex-wrap items-end gap-4">
<div className="flex flex-col gap-1">
<label
htmlFor="history-kind"
className="font-mono text-[10px] uppercase tracking-widest text-outline"
>
Type
</label>
<select
id="history-kind"
value={filterKind}
onChange={(event) => setFilterKind(event.target.value as ActivityKind | 'all')}
className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary"
>
<option value="all">All Types</option>
<option value="stealth-send">Stealth Send</option>
<option value="stealth-receive">Stealth Receive</option>
<option value="withdrawal">Withdrawal</option>
<option value="name-registration">Name Registration</option>
<option value="vault-claim">Vault Claim</option>
</select>
</div>

<div className="flex flex-col gap-1">
<label
htmlFor="history-status"
className="font-mono text-[10px] uppercase tracking-widest text-outline"
>
Status
</label>
<select
id="history-status"
value={filterStatus}
onChange={(event) => setFilterStatus(event.target.value as ActivityStatus | 'all')}
className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary"
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="failed">Failed</option>
</select>
</div>

<div className="flex flex-col gap-1">
<label
htmlFor="history-chain"
className="font-mono text-[10px] uppercase tracking-widest text-outline"
>
Chain
</label>
<select
id="history-chain"
value={filterChain}
onChange={(event) => setFilterChain(event.target.value as HistoryChain)}
className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary"
>
{CHAINS.map((chain) => (
<option key={chain} value={chain}>
{chain === 'all'
? 'All Chains'
: `${chain.charAt(0).toUpperCase()}${chain.slice(1)}`}
</option>
))}
</select>
</div>

<span className="pb-2 font-mono text-[10px] uppercase tracking-widest text-outline">
{filteredEntries.length} {filteredEntries.length === 1 ? 'entry' : 'entries'}
</span>
</div>

<div className="flex flex-wrap gap-2 border-t border-outline-variant pt-4">
<button
type="button"
onClick={() => downloadActivityCsv(exportEntries)}
disabled={filteredEntries.length === 0}
className="border border-outline px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:border-tertiary hover:text-tertiary disabled:cursor-not-allowed disabled:opacity-40"
>
Export CSV
</button>
<button
type="button"
onClick={() => downloadActivityJson(exportEntries)}
disabled={filteredEntries.length === 0}
className="border border-outline px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:border-tertiary hover:text-tertiary disabled:cursor-not-allowed disabled:opacity-40"
>
<rect x="6" y="6" width="36" height="36" rx="0" />
<line x1="14" y1="18" x2="34" y2="18" />
<line x1="14" y1="24" x2="30" y2="24" />
<line x1="14" y1="30" x2="26" y2="30" />
<circle cx="36" cy="34" r="6" fill="currentColor" opacity="0.2" />
<path d="M34 34l2 2 4-4" />
</svg>
}
title="History coming soon"
description={`Transaction history for ${chain.charAt(0).toUpperCase() + chain.slice(1)} is not yet available. Switch to Stellar to view activity.`}
primaryCTA={{ label: 'Send your first payment', onClick: () => navigate('/send') }}
/>
</div>
Export JSON
</button>
<p className="basis-full font-body text-xs text-outline">
Exports include chain, status, amount, timestamp, and explorer links only. Private key
and memo material is never included.
</p>
</div>
</div>

{entries.length === 0 && (
<EmptyState
illustration={
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="6" y="6" width="36" height="36" rx="0" />
<line x1="14" y1="18" x2="34" y2="18" />
<line x1="14" y1="24" x2="30" y2="24" />
<line x1="14" y1="30" x2="26" y2="30" />
<circle cx="36" cy="34" r="6" fill="currentColor" opacity="0.2" />
<path d="M34 34l2 2 4-4" />
</svg>
}
title="No activity recorded yet."
description="Send or receive a stealth payment to see your history here."
primaryCTA={{ label: 'Send your first payment', onClick: () => navigate('/send') }}
/>
)}

{entries.length > 0 && filteredEntries.length === 0 && (
<EmptyState
title="No matches"
description="No activity matches the current filters. Try selecting a different type, status, or chain."
/>
)}

<div className="flex flex-col gap-4">
{filteredEntries.map((entry: ActivityEntry) => (
<ActivityRow key={entry.id} entry={entry} />
))}
</div>
</section>
);
}
2 changes: 2 additions & 0 deletions src/stores/activityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface ActivityState {
addEntry: (entry: ActivityEntry) => void;
updateStatus: (id: string, status: ActivityStatus) => void;
clearHistory: (chain: string, wallet: string) => void;
clearAll: () => void;
pollPending: () => Promise<void>;
}

Expand All @@ -52,6 +53,7 @@ export const useActivityStore = create<ActivityState>()(
set((state) => ({
entries: state.entries.filter((e) => !(e.chain === chain && e.wallet === wallet)),
})),
clearAll: () => set({ entries: [] }),
pollPending: async () => {
const { entries, updateStatus } = get();
const pendingTxs = entries.filter((e) => e.status === 'pending' && e.chain === 'stellar');
Expand Down
Loading