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
130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
|
import { Copy, Check, Plus, QrCode } from 'lucide-react';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { useWalletStore } from '../store/wallet';
|
|
|
|
export default function Receive() {
|
|
const { addresses, refreshAddresses } = useWalletStore();
|
|
const [copied, setCopied] = useState<string | null>(null);
|
|
const [generating, setGenerating] = useState(false);
|
|
|
|
const handleCopy = async (address: string) => {
|
|
await writeText(address);
|
|
setCopied(address);
|
|
setTimeout(() => setCopied(null), 2000);
|
|
};
|
|
|
|
const handleGenerateAddress = async () => {
|
|
setGenerating(true);
|
|
try {
|
|
await invoke('generate_address', { label: null });
|
|
await refreshAddresses();
|
|
} catch (error) {
|
|
console.error('Failed to generate address:', error);
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
};
|
|
|
|
const primaryAddress = addresses[0]?.address;
|
|
|
|
return (
|
|
<div className="max-w-lg">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold text-white">Receive SYN</h1>
|
|
<button
|
|
onClick={handleGenerateAddress}
|
|
disabled={generating}
|
|
className="btn btn-secondary"
|
|
>
|
|
<Plus size={18} />
|
|
New Address
|
|
</button>
|
|
</div>
|
|
|
|
{/* Primary address with QR placeholder */}
|
|
{primaryAddress && (
|
|
<div className="card mb-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<QrCode size={18} className="text-gray-400" />
|
|
<span className="text-sm text-gray-400">Primary Address</span>
|
|
</div>
|
|
|
|
{/* QR Code placeholder */}
|
|
<div className="w-48 h-48 mx-auto mb-4 bg-white rounded-lg flex items-center justify-center">
|
|
<div className="text-gray-400 text-center p-4">
|
|
<QrCode size={64} className="mx-auto mb-2 text-gray-300" />
|
|
<span className="text-xs">QR Code</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Address with copy */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 bg-gray-950 rounded-lg p-3">
|
|
<p className="font-mono text-sm text-gray-300 break-all">
|
|
{primaryAddress}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleCopy(primaryAddress)}
|
|
className="btn btn-secondary shrink-0"
|
|
>
|
|
{copied === primaryAddress ? (
|
|
<Check size={18} className="text-green-400" />
|
|
) : (
|
|
<Copy size={18} />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* All addresses */}
|
|
<div className="card">
|
|
<h3 className="font-semibold text-white mb-4">All Addresses</h3>
|
|
<div className="space-y-3">
|
|
{addresses.map((addr, i) => (
|
|
<div
|
|
key={addr.address}
|
|
className="flex items-center gap-2 p-3 bg-gray-950 rounded-lg"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xs text-gray-500">#{addr.index}</span>
|
|
{addr.label && (
|
|
<span className="text-xs text-synor-400">{addr.label}</span>
|
|
)}
|
|
{i === 0 && (
|
|
<span className="text-xs bg-synor-600 text-white px-1.5 py-0.5 rounded">
|
|
Primary
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="font-mono text-xs text-gray-400 truncate">
|
|
{addr.address}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleCopy(addr.address)}
|
|
className="p-2 hover:bg-gray-800 rounded transition-colors text-gray-400 hover:text-white"
|
|
>
|
|
{copied === addr.address ? (
|
|
<Check size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} />
|
|
)}
|
|
</button>
|
|
</div>
|
|
))}
|
|
|
|
{addresses.length === 0 && (
|
|
<p className="text-gray-500 text-sm text-center py-4">
|
|
No addresses yet. Generate one to receive funds.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|