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
175 lines
5.3 KiB
TypeScript
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
|
|
}),
|
|
}
|
|
)
|
|
);
|