synor/apps/desktop-wallet/src/store/wallet.ts
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

175 lines
5.3 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
/**
* Sanitized error logging - prevents sensitive data from being logged
* Only logs generic error types in production, full errors in development
*/
function logError(context: string, error: unknown): void {
// In production, only log the context and error type, not the full message
// which might contain sensitive information like addresses or amounts
if (import.meta.env.PROD) {
const errorType = error instanceof Error ? error.name : 'Unknown';
console.error(`[Wallet] ${context}: ${errorType}`);
} else {
// In development, log full error for debugging (but still sanitize)
const safeError = error instanceof Error ? error.message : 'Unknown error';
// Remove any potential sensitive patterns (addresses, keys, etc.)
const sanitized = safeError
.replace(/synor1[a-z0-9]{38,}/gi, '[ADDRESS]')
.replace(/tsynor1[a-z0-9]{38,}/gi, '[ADDRESS]')
.replace(/[a-f0-9]{64}/gi, '[HASH]')
.replace(/\b[a-z]{3,}\s+[a-z]{3,}\s+[a-z]{3,}/gi, '[POSSIBLE_MNEMONIC]');
console.error(`[Wallet] ${context}:`, sanitized);
}
}
export interface WalletAddress {
address: string;
index: number;
isChange: boolean;
label?: string;
}
export interface Balance {
balance: number;
balanceHuman: string;
pending: number;
}
export interface NetworkStatus {
connected: boolean;
network?: string;
blockHeight?: number;
peerCount?: number;
synced?: boolean;
}
interface WalletState {
// State
isInitialized: boolean;
isUnlocked: boolean;
addresses: WalletAddress[];
balance: Balance | null;
networkStatus: NetworkStatus;
// Actions
setInitialized: (initialized: boolean) => void;
setUnlocked: (unlocked: boolean) => void;
setAddresses: (addresses: WalletAddress[]) => void;
setBalance: (balance: Balance | null) => void;
setNetworkStatus: (status: NetworkStatus) => void;
// Async actions (invoke Tauri commands)
createWallet: (password: string) => Promise<{ mnemonic: string; address: string }>;
importWallet: (mnemonic: string, password: string) => Promise<string>;
unlockWallet: (password: string) => Promise<boolean>;
lockWallet: () => Promise<void>;
refreshBalance: () => Promise<void>;
refreshAddresses: () => Promise<void>;
connectNode: (rpcUrl: string, wsUrl?: string) => Promise<void>;
}
export const useWalletStore = create<WalletState>()(
persist(
(set, get) => ({
// Initial state
isInitialized: false,
isUnlocked: false,
addresses: [],
balance: null,
networkStatus: { connected: false },
// Sync setters
setInitialized: (initialized) => set({ isInitialized: initialized }),
setUnlocked: (unlocked) => set({ isUnlocked: unlocked }),
setAddresses: (addresses) => set({ addresses }),
setBalance: (balance) => set({ balance }),
setNetworkStatus: (status) => set({ networkStatus: status }),
// Async actions
createWallet: async (password: string) => {
const result = await invoke<{ mnemonic: string; address: string }>(
'create_wallet',
{ password }
);
set({
isInitialized: true,
isUnlocked: true,
addresses: [{ address: result.address, index: 0, isChange: false }],
});
return result;
},
importWallet: async (mnemonic: string, password: string) => {
const address = await invoke<string>('import_wallet', {
request: { mnemonic, password },
});
set({
isInitialized: true,
isUnlocked: true,
addresses: [{ address, index: 0, isChange: false }],
});
return address;
},
unlockWallet: async (password: string) => {
const success = await invoke<boolean>('unlock_wallet', { password });
if (success) {
set({ isUnlocked: true });
// Refresh data after unlock
get().refreshBalance();
get().refreshAddresses();
}
return success;
},
lockWallet: async () => {
await invoke('lock_wallet');
set({
isUnlocked: false,
balance: null,
});
},
refreshBalance: async () => {
try {
const balance = await invoke<Balance>('get_balance');
set({ balance });
} catch (error) {
logError('refreshBalance', error);
}
},
refreshAddresses: async () => {
try {
const addresses = await invoke<WalletAddress[]>('get_addresses');
set({ addresses });
} catch (error) {
logError('refreshAddresses', error);
}
},
connectNode: async (rpcUrl: string, wsUrl?: string) => {
try {
const connection = await invoke<NetworkStatus>('connect_node', {
rpcUrl,
wsUrl,
});
set({ networkStatus: { ...connection, connected: true } });
} catch (error) {
logError('connectNode', error);
set({ networkStatus: { connected: false } });
}
},
}),
{
name: 'synor-wallet-storage',
partialize: (state) => ({
isInitialized: state.isInitialized,
// Don't persist sensitive state like isUnlocked
}),
}
)
);