synor/apps/desktop-wallet/src/pages/Receive.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

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>
);
}