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
161 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|