synor/apps/desktop-wallet/src/pages/History.tsx
Gulshan Yadav 6b5a232a5e feat: Desktop wallet, gas estimator UI, and 30-day monitoring stack
Security (Desktop Wallet):
- Implement BIP39 mnemonic generation with cryptographic RNG
- Add Argon2id password-based key derivation (64MB, 3 iterations)
- Add ChaCha20-Poly1305 authenticated encryption for seed storage
- Add mnemonic auto-clear (60s timeout) and clipboard auto-clear (30s)
- Add sanitized error logging to prevent credential leaks
- Strengthen CSP with object-src, base-uri, form-action, frame-ancestors
- Clear sensitive state on component unmount

Explorer (Gas Estimator):
- Add Gas Estimation page with from/to/amount/data inputs
- Add bech32 address validation (synor1/tsynor1 prefix)
- Add BigInt-based amount parsing to avoid floating point errors
- Add production guard for mock mode (cannot enable in prod builds)

Monitoring (30-day Testnet):
- Add Prometheus config with 30-day retention
- Add comprehensive alert rules for node health, consensus, network, mempool
- Add Alertmanager with severity-based routing and inhibition rules
- Add Grafana with auto-provisioned datasource and dashboard
- Add Synor testnet dashboard with uptime SLA tracking

Docker:
- Update docker-compose.testnet.yml with monitoring profile
- Fix node-exporter for macOS Docker Desktop compatibility
- Change Grafana port to 3001 to avoid conflict
2026-01-10 04:38:09 +05:30

161 lines
5.2 KiB
TypeScript

import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import {
ArrowUpRight,
ArrowDownLeft,
ExternalLink,
RefreshCw,
} from 'lucide-react';
interface Transaction {
txid: string;
direction: 'sent' | 'received';
amount: number;
fee?: number;
timestamp: number;
confirmations: number;
counterparty?: string;
}
export default function History() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(true);
const fetchHistory = async () => {
setLoading(true);
try {
const history = await invoke<Transaction[]>('get_transaction_history', {
limit: 50,
});
setTransactions(history);
} catch (error) {
console.error('Failed to fetch history:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchHistory();
}, []);
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const formatAmount = (amount: number, direction: string) => {
const syn = amount / 100_000_000;
const sign = direction === 'received' ? '+' : '-';
return `${sign}${syn.toFixed(8)} SYN`;
};
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-white">Transaction History</h1>
<button onClick={fetchHistory} className="btn btn-ghost" disabled={loading}>
<RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
</button>
</div>
<div className="card">
{loading && transactions.length === 0 ? (
<div className="text-center py-12">
<RefreshCw size={24} className="animate-spin mx-auto text-gray-500 mb-2" />
<p className="text-gray-500">Loading transactions...</p>
</div>
) : transactions.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500">No transactions yet.</p>
<p className="text-gray-600 text-sm mt-1">
Transactions will appear here once you send or receive SYN.
</p>
</div>
) : (
<div className="divide-y divide-gray-800">
{transactions.map((tx) => (
<div
key={tx.txid}
className="py-4 first:pt-0 last:pb-0 hover:bg-gray-800/50 -mx-6 px-6 transition-colors"
>
<div className="flex items-center gap-4">
{/* Icon */}
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
tx.direction === 'received'
? 'bg-green-600/20 text-green-400'
: 'bg-synor-600/20 text-synor-400'
}`}
>
{tx.direction === 'received' ? (
<ArrowDownLeft size={20} />
) : (
<ArrowUpRight size={20} />
)}
</div>
{/* Details */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-white capitalize">
{tx.direction}
</span>
{tx.confirmations < 10 && (
<span className="text-xs bg-yellow-600/20 text-yellow-400 px-1.5 py-0.5 rounded">
{tx.confirmations} conf
</span>
)}
</div>
<p className="text-sm text-gray-500">
{formatDate(tx.timestamp)}
</p>
</div>
{/* Amount */}
<div className="text-right">
<p
className={`font-mono font-medium ${
tx.direction === 'received'
? 'text-green-400'
: 'text-white'
}`}
>
{formatAmount(tx.amount, tx.direction)}
</p>
{tx.fee && (
<p className="text-xs text-gray-500">
Fee: {(tx.fee / 100_000_000).toFixed(8)} SYN
</p>
)}
</div>
{/* External link */}
<a
href={`https://explorer.synor.io/tx/${tx.txid}`}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-gray-500 hover:text-white transition-colors"
title="View in explorer"
>
<ExternalLink size={16} />
</a>
</div>
{/* TXID */}
<p className="font-mono text-xs text-gray-600 mt-2 truncate">
{tx.txid}
</p>
</div>
))}
</div>
)}
</div>
</div>
);
}